##// END OF EJS Templates
merge: stable into default
Raphaël Gomès -
r49869:12adf8c6 merge default
parent child Browse files
Show More
@@ -1,1372 +1,1372 b''
1 1 # dispatch.py - command dispatching for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8
9 9 import errno
10 10 import getopt
11 11 import io
12 12 import os
13 13 import pdb
14 14 import re
15 15 import signal
16 16 import sys
17 17 import traceback
18 18
19 19
20 20 from .i18n import _
21 21 from .pycompat import getattr
22 22
23 23 from hgdemandimport import tracing
24 24
25 25 from . import (
26 26 cmdutil,
27 27 color,
28 28 commands,
29 29 demandimport,
30 30 encoding,
31 31 error,
32 32 extensions,
33 33 fancyopts,
34 34 help,
35 35 hg,
36 36 hook,
37 37 localrepo,
38 38 profiling,
39 39 pycompat,
40 40 rcutil,
41 41 registrar,
42 42 requirements as requirementsmod,
43 43 scmutil,
44 44 ui as uimod,
45 45 util,
46 46 vfs,
47 47 )
48 48
49 49 from .utils import (
50 50 procutil,
51 51 stringutil,
52 52 urlutil,
53 53 )
54 54
55 55
56 56 class request:
57 57 def __init__(
58 58 self,
59 59 args,
60 60 ui=None,
61 61 repo=None,
62 62 fin=None,
63 63 fout=None,
64 64 ferr=None,
65 65 fmsg=None,
66 66 prereposetups=None,
67 67 ):
68 68 self.args = args
69 69 self.ui = ui
70 70 self.repo = repo
71 71
72 72 # input/output/error streams
73 73 self.fin = fin
74 74 self.fout = fout
75 75 self.ferr = ferr
76 76 # separate stream for status/error messages
77 77 self.fmsg = fmsg
78 78
79 79 # remember options pre-parsed by _earlyparseopts()
80 80 self.earlyoptions = {}
81 81
82 82 # reposetups which run before extensions, useful for chg to pre-fill
83 83 # low-level repo state (for example, changelog) before extensions.
84 84 self.prereposetups = prereposetups or []
85 85
86 86 # store the parsed and canonical command
87 87 self.canonical_command = None
88 88
89 89 def _runexithandlers(self):
90 90 exc = None
91 91 handlers = self.ui._exithandlers
92 92 try:
93 93 while handlers:
94 94 func, args, kwargs = handlers.pop()
95 95 try:
96 96 func(*args, **kwargs)
97 97 except: # re-raises below
98 98 if exc is None:
99 99 exc = sys.exc_info()[1]
100 100 self.ui.warnnoi18n(b'error in exit handlers:\n')
101 101 self.ui.traceback(force=True)
102 102 finally:
103 103 if exc is not None:
104 104 raise exc
105 105
106 106
107 107 def _flushstdio(ui, err):
108 108 status = None
109 109 # In all cases we try to flush stdio streams.
110 110 if util.safehasattr(ui, b'fout'):
111 111 assert ui is not None # help pytype
112 112 assert ui.fout is not None # help pytype
113 113 try:
114 114 ui.fout.flush()
115 115 except IOError as e:
116 116 err = e
117 117 status = -1
118 118
119 119 if util.safehasattr(ui, b'ferr'):
120 120 assert ui is not None # help pytype
121 121 assert ui.ferr is not None # help pytype
122 122 try:
123 123 if err is not None and err.errno != errno.EPIPE:
124 124 ui.ferr.write(
125 125 b'abort: %s\n' % encoding.strtolocal(err.strerror)
126 126 )
127 127 ui.ferr.flush()
128 128 # There's not much we can do about an I/O error here. So (possibly)
129 129 # change the status code and move on.
130 130 except IOError:
131 131 status = -1
132 132
133 133 return status
134 134
135 135
136 136 def run():
137 137 """run the command in sys.argv"""
138 138 try:
139 139 initstdio()
140 140 with tracing.log('parse args into request'):
141 141 req = request(pycompat.sysargv[1:])
142 142
143 143 status = dispatch(req)
144 144 _silencestdio()
145 145 except KeyboardInterrupt:
146 146 # Catch early/late KeyboardInterrupt as last ditch. Here nothing will
147 147 # be printed to console to avoid another IOError/KeyboardInterrupt.
148 148 status = -1
149 149 sys.exit(status & 255)
150 150
151 151
152 152 def initstdio():
153 153 # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
154 154 # buffer. These streams will normalize \n to \r\n by default. Mercurial's
155 155 # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
156 156 # instances, which write to the underlying stdio file descriptor in binary
157 157 # mode. ui.write() uses \n for line endings and no line ending normalization
158 158 # is attempted through this interface. This "just works," even if the system
159 159 # preferred line ending is not \n.
160 160 #
161 161 # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
162 162 # and sys.stderr. They will inherit the line ending normalization settings,
163 163 # potentially causing e.g. \r\n to be emitted. Since emitting \n should
164 164 # "just work," here we change the sys.* streams to disable line ending
165 165 # normalization, ensuring compatibility with our ui type.
166 166
167 167 if sys.stdout is not None:
168 168 # write_through is new in Python 3.7.
169 169 kwargs = {
170 170 "newline": "\n",
171 171 "line_buffering": sys.stdout.line_buffering,
172 172 }
173 173 if util.safehasattr(sys.stdout, "write_through"):
174 174 # pytype: disable=attribute-error
175 175 kwargs["write_through"] = sys.stdout.write_through
176 176 # pytype: enable=attribute-error
177 177 sys.stdout = io.TextIOWrapper(
178 178 sys.stdout.buffer, sys.stdout.encoding, sys.stdout.errors, **kwargs
179 179 )
180 180
181 181 if sys.stderr is not None:
182 182 kwargs = {
183 183 "newline": "\n",
184 184 "line_buffering": sys.stderr.line_buffering,
185 185 }
186 186 if util.safehasattr(sys.stderr, "write_through"):
187 187 # pytype: disable=attribute-error
188 188 kwargs["write_through"] = sys.stderr.write_through
189 189 # pytype: enable=attribute-error
190 190 sys.stderr = io.TextIOWrapper(
191 191 sys.stderr.buffer, sys.stderr.encoding, sys.stderr.errors, **kwargs
192 192 )
193 193
194 194 if sys.stdin is not None:
195 195 # No write_through on read-only stream.
196 196 sys.stdin = io.TextIOWrapper(
197 197 sys.stdin.buffer,
198 198 sys.stdin.encoding,
199 199 sys.stdin.errors,
200 200 # None is universal newlines mode.
201 201 newline=None,
202 202 line_buffering=sys.stdin.line_buffering,
203 203 )
204 204
205 205
206 206 def _silencestdio():
207 207 for fp in (sys.stdout, sys.stderr):
208 208 if fp is None:
209 209 continue
210 210 # Check if the file is okay
211 211 try:
212 212 fp.flush()
213 213 continue
214 214 except IOError:
215 215 pass
216 216 # Otherwise mark it as closed to silence "Exception ignored in"
217 217 # message emitted by the interpreter finalizer.
218 218 try:
219 219 fp.close()
220 220 except IOError:
221 221 pass
222 222
223 223
224 224 def _formatargs(args):
225 225 return b' '.join(procutil.shellquote(a) for a in args)
226 226
227 227
228 228 def dispatch(req):
229 229 """run the command specified in req.args; returns an integer status code"""
230 230 err = None
231 231 try:
232 232 status = _rundispatch(req)
233 233 except error.StdioError as e:
234 234 err = e
235 235 status = -1
236 236
237 237 ret = _flushstdio(req.ui, err)
238 238 if ret and not status:
239 239 status = ret
240 240 return status
241 241
242 242
243 243 def _rundispatch(req):
244 244 with tracing.log('dispatch._rundispatch'):
245 245 if req.ferr:
246 246 ferr = req.ferr
247 247 elif req.ui:
248 248 ferr = req.ui.ferr
249 249 else:
250 250 ferr = procutil.stderr
251 251
252 252 try:
253 253 if not req.ui:
254 254 req.ui = uimod.ui.load()
255 255 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
256 256 if req.earlyoptions[b'traceback']:
257 257 req.ui.setconfig(b'ui', b'traceback', b'on', b'--traceback')
258 258
259 259 # set ui streams from the request
260 260 if req.fin:
261 261 req.ui.fin = req.fin
262 262 if req.fout:
263 263 req.ui.fout = req.fout
264 264 if req.ferr:
265 265 req.ui.ferr = req.ferr
266 266 if req.fmsg:
267 267 req.ui.fmsg = req.fmsg
268 268 except error.Abort as inst:
269 269 ferr.write(inst.format())
270 270 return -1
271 271
272 msg = _formatargs(req.args)
272 formattedargs = _formatargs(req.args)
273 273 starttime = util.timer()
274 274 ret = 1 # default of Python exit code on unhandled exception
275 275 try:
276 276 ret = _runcatch(req) or 0
277 277 except error.ProgrammingError as inst:
278 278 req.ui.error(_(b'** ProgrammingError: %s\n') % inst)
279 279 if inst.hint:
280 280 req.ui.error(_(b'** (%s)\n') % inst.hint)
281 281 raise
282 282 except KeyboardInterrupt as inst:
283 283 try:
284 284 if isinstance(inst, error.SignalInterrupt):
285 285 msg = _(b"killed!\n")
286 286 else:
287 287 msg = _(b"interrupted!\n")
288 288 req.ui.error(msg)
289 289 except error.SignalInterrupt:
290 290 # maybe pager would quit without consuming all the output, and
291 291 # SIGPIPE was raised. we cannot print anything in this case.
292 292 pass
293 293 except IOError as inst:
294 294 if inst.errno != errno.EPIPE:
295 295 raise
296 296 ret = -1
297 297 finally:
298 298 duration = util.timer() - starttime
299 299 req.ui.flush() # record blocked times
300 300 if req.ui.logblockedtimes:
301 301 req.ui._blockedtimes[b'command_duration'] = duration * 1000
302 302 req.ui.log(
303 303 b'uiblocked',
304 304 b'ui blocked ms\n',
305 305 **pycompat.strkwargs(req.ui._blockedtimes)
306 306 )
307 307 return_code = ret & 255
308 308 req.ui.log(
309 309 b"commandfinish",
310 310 b"%s exited %d after %0.2f seconds\n",
311 msg,
311 formattedargs,
312 312 return_code,
313 313 duration,
314 314 return_code=return_code,
315 315 duration=duration,
316 316 canonical_command=req.canonical_command,
317 317 )
318 318 try:
319 319 req._runexithandlers()
320 320 except: # exiting, so no re-raises
321 321 ret = ret or -1
322 322 # do flush again since ui.log() and exit handlers may write to ui
323 323 req.ui.flush()
324 324 return ret
325 325
326 326
327 327 def _runcatch(req):
328 328 with tracing.log('dispatch._runcatch'):
329 329
330 330 def catchterm(*args):
331 331 raise error.SignalInterrupt
332 332
333 333 ui = req.ui
334 334 try:
335 335 for name in b'SIGBREAK', b'SIGHUP', b'SIGTERM':
336 336 num = getattr(signal, name, None)
337 337 if num:
338 338 signal.signal(num, catchterm)
339 339 except ValueError:
340 340 pass # happens if called in a thread
341 341
342 342 def _runcatchfunc():
343 343 realcmd = None
344 344 try:
345 345 cmdargs = fancyopts.fancyopts(
346 346 req.args[:], commands.globalopts, {}
347 347 )
348 348 cmd = cmdargs[0]
349 349 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
350 350 realcmd = aliases[0]
351 351 except (
352 352 error.UnknownCommand,
353 353 error.AmbiguousCommand,
354 354 IndexError,
355 355 getopt.GetoptError,
356 356 ):
357 357 # Don't handle this here. We know the command is
358 358 # invalid, but all we're worried about for now is that
359 359 # it's not a command that server operators expect to
360 360 # be safe to offer to users in a sandbox.
361 361 pass
362 362 if realcmd == b'serve' and b'--stdio' in cmdargs:
363 363 # We want to constrain 'hg serve --stdio' instances pretty
364 364 # closely, as many shared-ssh access tools want to grant
365 365 # access to run *only* 'hg -R $repo serve --stdio'. We
366 366 # restrict to exactly that set of arguments, and prohibit
367 367 # any repo name that starts with '--' to prevent
368 368 # shenanigans wherein a user does something like pass
369 369 # --debugger or --config=ui.debugger=1 as a repo
370 370 # name. This used to actually run the debugger.
371 371 if (
372 372 len(req.args) != 4
373 373 or req.args[0] != b'-R'
374 374 or req.args[1].startswith(b'--')
375 375 or req.args[2] != b'serve'
376 376 or req.args[3] != b'--stdio'
377 377 ):
378 378 raise error.Abort(
379 379 _(b'potentially unsafe serve --stdio invocation: %s')
380 380 % (stringutil.pprint(req.args),)
381 381 )
382 382
383 383 try:
384 384 debugger = b'pdb'
385 385 debugtrace = {b'pdb': pdb.set_trace}
386 386 debugmortem = {b'pdb': pdb.post_mortem}
387 387
388 388 # read --config before doing anything else
389 389 # (e.g. to change trust settings for reading .hg/hgrc)
390 390 cfgs = _parseconfig(req.ui, req.earlyoptions[b'config'])
391 391
392 392 if req.repo:
393 393 # copy configs that were passed on the cmdline (--config) to
394 394 # the repo ui
395 395 for sec, name, val in cfgs:
396 396 req.repo.ui.setconfig(
397 397 sec, name, val, source=b'--config'
398 398 )
399 399
400 400 # developer config: ui.debugger
401 401 debugger = ui.config(b"ui", b"debugger")
402 402 debugmod = pdb
403 403 if not debugger or ui.plain():
404 404 # if we are in HGPLAIN mode, then disable custom debugging
405 405 debugger = b'pdb'
406 406 elif req.earlyoptions[b'debugger']:
407 407 # This import can be slow for fancy debuggers, so only
408 408 # do it when absolutely necessary, i.e. when actual
409 409 # debugging has been requested
410 410 with demandimport.deactivated():
411 411 try:
412 412 debugmod = __import__(debugger)
413 413 except ImportError:
414 414 pass # Leave debugmod = pdb
415 415
416 416 debugtrace[debugger] = debugmod.set_trace
417 417 debugmortem[debugger] = debugmod.post_mortem
418 418
419 419 # enter the debugger before command execution
420 420 if req.earlyoptions[b'debugger']:
421 421 ui.warn(
422 422 _(
423 423 b"entering debugger - "
424 424 b"type c to continue starting hg or h for help\n"
425 425 )
426 426 )
427 427
428 428 if (
429 429 debugger != b'pdb'
430 430 and debugtrace[debugger] == debugtrace[b'pdb']
431 431 ):
432 432 ui.warn(
433 433 _(
434 434 b"%s debugger specified "
435 435 b"but its module was not found\n"
436 436 )
437 437 % debugger
438 438 )
439 439 with demandimport.deactivated():
440 440 debugtrace[debugger]()
441 441 try:
442 442 return _dispatch(req)
443 443 finally:
444 444 ui.flush()
445 445 except: # re-raises
446 446 # enter the debugger when we hit an exception
447 447 if req.earlyoptions[b'debugger']:
448 448 traceback.print_exc()
449 449 debugmortem[debugger](sys.exc_info()[2])
450 450 raise
451 451
452 452 return _callcatch(ui, _runcatchfunc)
453 453
454 454
455 455 def _callcatch(ui, func):
456 456 """like scmutil.callcatch but handles more high-level exceptions about
457 457 config parsing and commands. besides, use handlecommandexception to handle
458 458 uncaught exceptions.
459 459 """
460 460 detailed_exit_code = -1
461 461 try:
462 462 return scmutil.callcatch(ui, func)
463 463 except error.AmbiguousCommand as inst:
464 464 detailed_exit_code = 10
465 465 ui.warn(
466 466 _(b"hg: command '%s' is ambiguous:\n %s\n")
467 467 % (inst.prefix, b" ".join(inst.matches))
468 468 )
469 469 except error.CommandError as inst:
470 470 detailed_exit_code = 10
471 471 if inst.command:
472 472 ui.pager(b'help')
473 473 msgbytes = pycompat.bytestr(inst.message)
474 474 ui.warn(_(b"hg %s: %s\n") % (inst.command, msgbytes))
475 475 commands.help_(ui, inst.command, full=False, command=True)
476 476 else:
477 477 ui.warn(_(b"hg: %s\n") % inst.message)
478 478 ui.warn(_(b"(use 'hg help -v' for a list of global options)\n"))
479 479 except error.UnknownCommand as inst:
480 480 detailed_exit_code = 10
481 481 nocmdmsg = _(b"hg: unknown command '%s'\n") % inst.command
482 482 try:
483 483 # check if the command is in a disabled extension
484 484 # (but don't check for extensions themselves)
485 485 formatted = help.formattedhelp(
486 486 ui, commands, inst.command, unknowncmd=True
487 487 )
488 488 ui.warn(nocmdmsg)
489 489 ui.write(formatted)
490 490 except (error.UnknownCommand, error.Abort):
491 491 suggested = False
492 492 if inst.all_commands:
493 493 sim = error.getsimilar(inst.all_commands, inst.command)
494 494 if sim:
495 495 ui.warn(nocmdmsg)
496 496 ui.warn(b"(%s)\n" % error.similarity_hint(sim))
497 497 suggested = True
498 498 if not suggested:
499 499 ui.warn(nocmdmsg)
500 500 ui.warn(_(b"(use 'hg help' for a list of commands)\n"))
501 501 except IOError:
502 502 raise
503 503 except KeyboardInterrupt:
504 504 raise
505 505 except: # probably re-raises
506 506 if not handlecommandexception(ui):
507 507 raise
508 508
509 509 if ui.configbool(b'ui', b'detailed-exit-code'):
510 510 return detailed_exit_code
511 511 else:
512 512 return -1
513 513
514 514
515 515 def aliasargs(fn, givenargs):
516 516 args = []
517 517 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
518 518 if not util.safehasattr(fn, b'_origfunc'):
519 519 args = getattr(fn, 'args', args)
520 520 if args:
521 521 cmd = b' '.join(map(procutil.shellquote, args))
522 522
523 523 nums = []
524 524
525 525 def replacer(m):
526 526 num = int(m.group(1)) - 1
527 527 nums.append(num)
528 528 if num < len(givenargs):
529 529 return givenargs[num]
530 530 raise error.InputError(_(b'too few arguments for command alias'))
531 531
532 532 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
533 533 givenargs = [x for i, x in enumerate(givenargs) if i not in nums]
534 534 args = pycompat.shlexsplit(cmd)
535 535 return args + givenargs
536 536
537 537
538 538 def aliasinterpolate(name, args, cmd):
539 539 """interpolate args into cmd for shell aliases
540 540
541 541 This also handles $0, $@ and "$@".
542 542 """
543 543 # util.interpolate can't deal with "$@" (with quotes) because it's only
544 544 # built to match prefix + patterns.
545 545 replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)}
546 546 replacemap[b'$0'] = name
547 547 replacemap[b'$$'] = b'$'
548 548 replacemap[b'$@'] = b' '.join(args)
549 549 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
550 550 # parameters, separated out into words. Emulate the same behavior here by
551 551 # quoting the arguments individually. POSIX shells will then typically
552 552 # tokenize each argument into exactly one word.
553 553 replacemap[b'"$@"'] = b' '.join(procutil.shellquote(arg) for arg in args)
554 554 # escape '\$' for regex
555 555 regex = b'|'.join(replacemap.keys()).replace(b'$', br'\$')
556 556 r = re.compile(regex)
557 557 return r.sub(lambda x: replacemap[x.group()], cmd)
558 558
559 559
560 560 class cmdalias:
561 561 def __init__(self, ui, name, definition, cmdtable, source):
562 562 self.name = self.cmd = name
563 563 self.cmdname = b''
564 564 self.definition = definition
565 565 self.fn = None
566 566 self.givenargs = []
567 567 self.opts = []
568 568 self.help = b''
569 569 self.badalias = None
570 570 self.unknowncmd = False
571 571 self.source = source
572 572
573 573 try:
574 574 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
575 575 for alias, e in cmdtable.items():
576 576 if e is entry:
577 577 self.cmd = alias
578 578 break
579 579 self.shadows = True
580 580 except error.UnknownCommand:
581 581 self.shadows = False
582 582
583 583 if not self.definition:
584 584 self.badalias = _(b"no definition for alias '%s'") % self.name
585 585 return
586 586
587 587 if self.definition.startswith(b'!'):
588 588 shdef = self.definition[1:]
589 589 self.shell = True
590 590
591 591 def fn(ui, *args):
592 592 env = {b'HG_ARGS': b' '.join((self.name,) + args)}
593 593
594 594 def _checkvar(m):
595 595 if m.groups()[0] == b'$':
596 596 return m.group()
597 597 elif int(m.groups()[0]) <= len(args):
598 598 return m.group()
599 599 else:
600 600 ui.debug(
601 601 b"No argument found for substitution "
602 602 b"of %i variable in alias '%s' definition.\n"
603 603 % (int(m.groups()[0]), self.name)
604 604 )
605 605 return b''
606 606
607 607 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
608 608 cmd = aliasinterpolate(self.name, args, cmd)
609 609 return ui.system(
610 610 cmd, environ=env, blockedtag=b'alias_%s' % self.name
611 611 )
612 612
613 613 self.fn = fn
614 614 self.alias = True
615 615 self._populatehelp(ui, name, shdef, self.fn)
616 616 return
617 617
618 618 try:
619 619 args = pycompat.shlexsplit(self.definition)
620 620 except ValueError as inst:
621 621 self.badalias = _(b"error in definition for alias '%s': %s") % (
622 622 self.name,
623 623 stringutil.forcebytestr(inst),
624 624 )
625 625 return
626 626 earlyopts, args = _earlysplitopts(args)
627 627 if earlyopts:
628 628 self.badalias = _(
629 629 b"error in definition for alias '%s': %s may "
630 630 b"only be given on the command line"
631 631 ) % (self.name, b'/'.join(pycompat.ziplist(*earlyopts)[0]))
632 632 return
633 633 self.cmdname = cmd = args.pop(0)
634 634 self.givenargs = args
635 635
636 636 try:
637 637 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
638 638 if len(tableentry) > 2:
639 639 self.fn, self.opts, cmdhelp = tableentry
640 640 else:
641 641 self.fn, self.opts = tableentry
642 642 cmdhelp = None
643 643
644 644 self.alias = True
645 645 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
646 646
647 647 except error.UnknownCommand:
648 648 self.badalias = _(
649 649 b"alias '%s' resolves to unknown command '%s'"
650 650 ) % (
651 651 self.name,
652 652 cmd,
653 653 )
654 654 self.unknowncmd = True
655 655 except error.AmbiguousCommand:
656 656 self.badalias = _(
657 657 b"alias '%s' resolves to ambiguous command '%s'"
658 658 ) % (
659 659 self.name,
660 660 cmd,
661 661 )
662 662
663 663 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
664 664 # confine strings to be passed to i18n.gettext()
665 665 cfg = {}
666 666 for k in (b'doc', b'help', b'category'):
667 667 v = ui.config(b'alias', b'%s:%s' % (name, k), None)
668 668 if v is None:
669 669 continue
670 670 if not encoding.isasciistr(v):
671 671 self.badalias = _(
672 672 b"non-ASCII character in alias definition '%s:%s'"
673 673 ) % (name, k)
674 674 return
675 675 cfg[k] = v
676 676
677 677 self.help = cfg.get(b'help', defaulthelp or b'')
678 678 if self.help and self.help.startswith(b"hg " + cmd):
679 679 # drop prefix in old-style help lines so hg shows the alias
680 680 self.help = self.help[4 + len(cmd) :]
681 681
682 682 self.owndoc = b'doc' in cfg
683 683 doc = cfg.get(b'doc', pycompat.getdoc(fn))
684 684 if doc is not None:
685 685 doc = pycompat.sysstr(doc)
686 686 self.__doc__ = doc
687 687
688 688 self.helpcategory = cfg.get(
689 689 b'category', registrar.command.CATEGORY_NONE
690 690 )
691 691
692 692 @property
693 693 def args(self):
694 694 args = pycompat.maplist(util.expandpath, self.givenargs)
695 695 return aliasargs(self.fn, args)
696 696
697 697 def __getattr__(self, name):
698 698 adefaults = {
699 699 'norepo': True,
700 700 'intents': set(),
701 701 'optionalrepo': False,
702 702 'inferrepo': False,
703 703 }
704 704 if name not in adefaults:
705 705 raise AttributeError(name)
706 706 if self.badalias or util.safehasattr(self, b'shell'):
707 707 return adefaults[name]
708 708 return getattr(self.fn, name)
709 709
710 710 def __call__(self, ui, *args, **opts):
711 711 if self.badalias:
712 712 hint = None
713 713 if self.unknowncmd:
714 714 try:
715 715 # check if the command is in a disabled extension
716 716 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
717 717 hint = _(b"'%s' is provided by '%s' extension") % (cmd, ext)
718 718 except error.UnknownCommand:
719 719 pass
720 720 raise error.ConfigError(self.badalias, hint=hint)
721 721 if self.shadows:
722 722 ui.debug(
723 723 b"alias '%s' shadows command '%s'\n" % (self.name, self.cmdname)
724 724 )
725 725
726 726 ui.log(
727 727 b'commandalias',
728 728 b"alias '%s' expands to '%s'\n",
729 729 self.name,
730 730 self.definition,
731 731 )
732 732 if util.safehasattr(self, b'shell'):
733 733 return self.fn(ui, *args, **opts)
734 734 else:
735 735 try:
736 736 return util.checksignature(self.fn)(ui, *args, **opts)
737 737 except error.SignatureError:
738 738 args = b' '.join([self.cmdname] + self.args)
739 739 ui.debug(b"alias '%s' expands to '%s'\n" % (self.name, args))
740 740 raise
741 741
742 742
743 743 class lazyaliasentry:
744 744 """like a typical command entry (func, opts, help), but is lazy"""
745 745
746 746 def __init__(self, ui, name, definition, cmdtable, source):
747 747 self.ui = ui
748 748 self.name = name
749 749 self.definition = definition
750 750 self.cmdtable = cmdtable.copy()
751 751 self.source = source
752 752 self.alias = True
753 753
754 754 @util.propertycache
755 755 def _aliasdef(self):
756 756 return cmdalias(
757 757 self.ui, self.name, self.definition, self.cmdtable, self.source
758 758 )
759 759
760 760 def __getitem__(self, n):
761 761 aliasdef = self._aliasdef
762 762 if n == 0:
763 763 return aliasdef
764 764 elif n == 1:
765 765 return aliasdef.opts
766 766 elif n == 2:
767 767 return aliasdef.help
768 768 else:
769 769 raise IndexError
770 770
771 771 def __iter__(self):
772 772 for i in range(3):
773 773 yield self[i]
774 774
775 775 def __len__(self):
776 776 return 3
777 777
778 778
779 779 def addaliases(ui, cmdtable):
780 780 # aliases are processed after extensions have been loaded, so they
781 781 # may use extension commands. Aliases can also use other alias definitions,
782 782 # but only if they have been defined prior to the current definition.
783 783 for alias, definition in ui.configitems(b'alias', ignoresub=True):
784 784 try:
785 785 if cmdtable[alias].definition == definition:
786 786 continue
787 787 except (KeyError, AttributeError):
788 788 # definition might not exist or it might not be a cmdalias
789 789 pass
790 790
791 791 source = ui.configsource(b'alias', alias)
792 792 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
793 793 cmdtable[alias] = entry
794 794
795 795
796 796 def _parse(ui, args):
797 797 options = {}
798 798 cmdoptions = {}
799 799
800 800 try:
801 801 args = fancyopts.fancyopts(args, commands.globalopts, options)
802 802 except getopt.GetoptError as inst:
803 803 raise error.CommandError(None, stringutil.forcebytestr(inst))
804 804
805 805 if args:
806 806 cmd, args = args[0], args[1:]
807 807 aliases, entry = cmdutil.findcmd(
808 808 cmd, commands.table, ui.configbool(b"ui", b"strict")
809 809 )
810 810 cmd = aliases[0]
811 811 args = aliasargs(entry[0], args)
812 812 defaults = ui.config(b"defaults", cmd)
813 813 if defaults:
814 814 args = (
815 815 pycompat.maplist(util.expandpath, pycompat.shlexsplit(defaults))
816 816 + args
817 817 )
818 818 c = list(entry[1])
819 819 else:
820 820 cmd = None
821 821 c = []
822 822
823 823 # combine global options into local
824 824 for o in commands.globalopts:
825 825 c.append((o[0], o[1], options[o[1]], o[3]))
826 826
827 827 try:
828 828 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
829 829 except getopt.GetoptError as inst:
830 830 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
831 831
832 832 # separate global options back out
833 833 for o in commands.globalopts:
834 834 n = o[1]
835 835 options[n] = cmdoptions[n]
836 836 del cmdoptions[n]
837 837
838 838 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
839 839
840 840
841 841 def _parseconfig(ui, config):
842 842 """parse the --config options from the command line"""
843 843 configs = []
844 844
845 845 for cfg in config:
846 846 try:
847 847 name, value = [cfgelem.strip() for cfgelem in cfg.split(b'=', 1)]
848 848 section, name = name.split(b'.', 1)
849 849 if not section or not name:
850 850 raise IndexError
851 851 ui.setconfig(section, name, value, b'--config')
852 852 configs.append((section, name, value))
853 853 except (IndexError, ValueError):
854 854 raise error.InputError(
855 855 _(
856 856 b'malformed --config option: %r '
857 857 b'(use --config section.name=value)'
858 858 )
859 859 % pycompat.bytestr(cfg)
860 860 )
861 861
862 862 return configs
863 863
864 864
865 865 def _earlyparseopts(ui, args):
866 866 options = {}
867 867 fancyopts.fancyopts(
868 868 args,
869 869 commands.globalopts,
870 870 options,
871 871 gnu=not ui.plain(b'strictflags'),
872 872 early=True,
873 873 optaliases={b'repository': [b'repo']},
874 874 )
875 875 return options
876 876
877 877
878 878 def _earlysplitopts(args):
879 879 """Split args into a list of possible early options and remainder args"""
880 880 shortoptions = b'R:'
881 881 # TODO: perhaps 'debugger' should be included
882 882 longoptions = [b'cwd=', b'repository=', b'repo=', b'config=']
883 883 return fancyopts.earlygetopt(
884 884 args, shortoptions, longoptions, gnu=True, keepsep=True
885 885 )
886 886
887 887
888 888 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
889 889 # run pre-hook, and abort if it fails
890 890 hook.hook(
891 891 lui,
892 892 repo,
893 893 b"pre-%s" % cmd,
894 894 True,
895 895 args=b" ".join(fullargs),
896 896 pats=cmdpats,
897 897 opts=cmdoptions,
898 898 )
899 899 try:
900 900 ret = _runcommand(ui, options, cmd, d)
901 901 # run post-hook, passing command result
902 902 hook.hook(
903 903 lui,
904 904 repo,
905 905 b"post-%s" % cmd,
906 906 False,
907 907 args=b" ".join(fullargs),
908 908 result=ret,
909 909 pats=cmdpats,
910 910 opts=cmdoptions,
911 911 )
912 912 except Exception:
913 913 # run failure hook and re-raise
914 914 hook.hook(
915 915 lui,
916 916 repo,
917 917 b"fail-%s" % cmd,
918 918 False,
919 919 args=b" ".join(fullargs),
920 920 pats=cmdpats,
921 921 opts=cmdoptions,
922 922 )
923 923 raise
924 924 return ret
925 925
926 926
927 927 def _readsharedsourceconfig(ui, path):
928 928 """if the current repository is shared one, this tries to read
929 929 .hg/hgrc of shared source if we are in share-safe mode
930 930
931 931 Config read is loaded into the ui object passed
932 932
933 933 This should be called before reading .hg/hgrc or the main repo
934 934 as that overrides config set in shared source"""
935 935 try:
936 936 with open(os.path.join(path, b".hg", b"requires"), "rb") as fp:
937 937 requirements = set(fp.read().splitlines())
938 938 if not (
939 939 requirementsmod.SHARESAFE_REQUIREMENT in requirements
940 940 and requirementsmod.SHARED_REQUIREMENT in requirements
941 941 ):
942 942 return
943 943 hgvfs = vfs.vfs(os.path.join(path, b".hg"))
944 944 sharedvfs = localrepo._getsharedvfs(hgvfs, requirements)
945 945 root = sharedvfs.base
946 946 ui.readconfig(sharedvfs.join(b"hgrc"), root)
947 947 except IOError:
948 948 pass
949 949
950 950
951 951 def _getlocal(ui, rpath, wd=None):
952 952 """Return (path, local ui object) for the given target path.
953 953
954 954 Takes paths in [cwd]/.hg/hgrc into account."
955 955 """
956 956 if wd is None:
957 957 try:
958 958 wd = encoding.getcwd()
959 959 except OSError as e:
960 960 raise error.Abort(
961 961 _(b"error getting current working directory: %s")
962 962 % encoding.strtolocal(e.strerror)
963 963 )
964 964
965 965 path = cmdutil.findrepo(wd) or b""
966 966 if not path:
967 967 lui = ui
968 968 else:
969 969 lui = ui.copy()
970 970 if rcutil.use_repo_hgrc():
971 971 _readsharedsourceconfig(lui, path)
972 972 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
973 973 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
974 974
975 975 if rpath:
976 976 path = urlutil.get_clone_path(lui, rpath)[0]
977 977 lui = ui.copy()
978 978 if rcutil.use_repo_hgrc():
979 979 _readsharedsourceconfig(lui, path)
980 980 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
981 981 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
982 982
983 983 return path, lui
984 984
985 985
986 986 def _checkshellalias(lui, ui, args):
987 987 """Return the function to run the shell alias, if it is required"""
988 988 options = {}
989 989
990 990 try:
991 991 args = fancyopts.fancyopts(args, commands.globalopts, options)
992 992 except getopt.GetoptError:
993 993 return
994 994
995 995 if not args:
996 996 return
997 997
998 998 cmdtable = commands.table
999 999
1000 1000 cmd = args[0]
1001 1001 try:
1002 1002 strict = ui.configbool(b"ui", b"strict")
1003 1003 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
1004 1004 except (error.AmbiguousCommand, error.UnknownCommand):
1005 1005 return
1006 1006
1007 1007 cmd = aliases[0]
1008 1008 fn = entry[0]
1009 1009
1010 1010 if cmd and util.safehasattr(fn, b'shell'):
1011 1011 # shell alias shouldn't receive early options which are consumed by hg
1012 1012 _earlyopts, args = _earlysplitopts(args)
1013 1013 d = lambda: fn(ui, *args[1:])
1014 1014 return lambda: runcommand(
1015 1015 lui, None, cmd, args[:1], ui, options, d, [], {}
1016 1016 )
1017 1017
1018 1018
1019 1019 def _dispatch(req):
1020 1020 args = req.args
1021 1021 ui = req.ui
1022 1022
1023 1023 # check for cwd
1024 1024 cwd = req.earlyoptions[b'cwd']
1025 1025 if cwd:
1026 1026 os.chdir(cwd)
1027 1027
1028 1028 rpath = req.earlyoptions[b'repository']
1029 1029 path, lui = _getlocal(ui, rpath)
1030 1030
1031 1031 uis = {ui, lui}
1032 1032
1033 1033 if req.repo:
1034 1034 uis.add(req.repo.ui)
1035 1035
1036 1036 if (
1037 1037 req.earlyoptions[b'verbose']
1038 1038 or req.earlyoptions[b'debug']
1039 1039 or req.earlyoptions[b'quiet']
1040 1040 ):
1041 1041 for opt in (b'verbose', b'debug', b'quiet'):
1042 1042 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
1043 1043 for ui_ in uis:
1044 1044 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1045 1045
1046 1046 if req.earlyoptions[b'profile']:
1047 1047 for ui_ in uis:
1048 1048 ui_.setconfig(b'profiling', b'enabled', b'true', b'--profile')
1049 1049 elif req.earlyoptions[b'profile'] is False:
1050 1050 # Check for it being set already, so that we don't pollute the config
1051 1051 # with this when using chg in the very common case that it's not
1052 1052 # enabled.
1053 1053 if lui.configbool(b'profiling', b'enabled'):
1054 1054 # Only do this on lui so that `chg foo` with a user config setting
1055 1055 # profiling.enabled=1 still shows profiling information (chg will
1056 1056 # specify `--no-profile` when `hg serve` is starting up, we don't
1057 1057 # want that to propagate to every later invocation).
1058 1058 lui.setconfig(b'profiling', b'enabled', b'false', b'--no-profile')
1059 1059
1060 1060 profile = lui.configbool(b'profiling', b'enabled')
1061 1061 with profiling.profile(lui, enabled=profile) as profiler:
1062 1062 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
1063 1063 # reposetup
1064 1064 extensions.loadall(lui)
1065 1065 # Propagate any changes to lui.__class__ by extensions
1066 1066 ui.__class__ = lui.__class__
1067 1067
1068 1068 # (uisetup and extsetup are handled in extensions.loadall)
1069 1069
1070 1070 # (reposetup is handled in hg.repository)
1071 1071
1072 1072 addaliases(lui, commands.table)
1073 1073
1074 1074 # All aliases and commands are completely defined, now.
1075 1075 # Check abbreviation/ambiguity of shell alias.
1076 1076 shellaliasfn = _checkshellalias(lui, ui, args)
1077 1077 if shellaliasfn:
1078 1078 # no additional configs will be set, set up the ui instances
1079 1079 for ui_ in uis:
1080 1080 extensions.populateui(ui_)
1081 1081 return shellaliasfn()
1082 1082
1083 1083 # check for fallback encoding
1084 1084 fallback = lui.config(b'ui', b'fallbackencoding')
1085 1085 if fallback:
1086 1086 encoding.fallbackencoding = fallback
1087 1087
1088 1088 fullargs = args
1089 1089 cmd, func, args, options, cmdoptions = _parse(lui, args)
1090 1090
1091 1091 # store the canonical command name in request object for later access
1092 1092 req.canonical_command = cmd
1093 1093
1094 1094 if options[b"config"] != req.earlyoptions[b"config"]:
1095 1095 raise error.InputError(_(b"option --config may not be abbreviated"))
1096 1096 if options[b"cwd"] != req.earlyoptions[b"cwd"]:
1097 1097 raise error.InputError(_(b"option --cwd may not be abbreviated"))
1098 1098 if options[b"repository"] != req.earlyoptions[b"repository"]:
1099 1099 raise error.InputError(
1100 1100 _(
1101 1101 b"option -R has to be separated from other options (e.g. not "
1102 1102 b"-qR) and --repository may only be abbreviated as --repo"
1103 1103 )
1104 1104 )
1105 1105 if options[b"debugger"] != req.earlyoptions[b"debugger"]:
1106 1106 raise error.InputError(
1107 1107 _(b"option --debugger may not be abbreviated")
1108 1108 )
1109 1109 # don't validate --profile/--traceback, which can be enabled from now
1110 1110
1111 1111 if options[b"encoding"]:
1112 1112 encoding.encoding = options[b"encoding"]
1113 1113 if options[b"encodingmode"]:
1114 1114 encoding.encodingmode = options[b"encodingmode"]
1115 1115 if options[b"time"]:
1116 1116
1117 1117 def get_times():
1118 1118 t = os.times()
1119 1119 if t[4] == 0.0:
1120 1120 # Windows leaves this as zero, so use time.perf_counter()
1121 1121 t = (t[0], t[1], t[2], t[3], util.timer())
1122 1122 return t
1123 1123
1124 1124 s = get_times()
1125 1125
1126 1126 def print_time():
1127 1127 t = get_times()
1128 1128 ui.warn(
1129 1129 _(b"time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n")
1130 1130 % (
1131 1131 t[4] - s[4],
1132 1132 t[0] - s[0],
1133 1133 t[2] - s[2],
1134 1134 t[1] - s[1],
1135 1135 t[3] - s[3],
1136 1136 )
1137 1137 )
1138 1138
1139 1139 ui.atexit(print_time)
1140 1140 if options[b"profile"]:
1141 1141 profiler.start()
1142 1142
1143 1143 # if abbreviated version of this were used, take them in account, now
1144 1144 if options[b'verbose'] or options[b'debug'] or options[b'quiet']:
1145 1145 for opt in (b'verbose', b'debug', b'quiet'):
1146 1146 if options[opt] == req.earlyoptions[opt]:
1147 1147 continue
1148 1148 val = pycompat.bytestr(bool(options[opt]))
1149 1149 for ui_ in uis:
1150 1150 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1151 1151
1152 1152 if options[b'traceback']:
1153 1153 for ui_ in uis:
1154 1154 ui_.setconfig(b'ui', b'traceback', b'on', b'--traceback')
1155 1155
1156 1156 if options[b'noninteractive']:
1157 1157 for ui_ in uis:
1158 1158 ui_.setconfig(b'ui', b'interactive', b'off', b'-y')
1159 1159
1160 1160 if cmdoptions.get(b'insecure', False):
1161 1161 for ui_ in uis:
1162 1162 ui_.insecureconnections = True
1163 1163
1164 1164 # setup color handling before pager, because setting up pager
1165 1165 # might cause incorrect console information
1166 1166 coloropt = options[b'color']
1167 1167 for ui_ in uis:
1168 1168 if coloropt:
1169 1169 ui_.setconfig(b'ui', b'color', coloropt, b'--color')
1170 1170 color.setup(ui_)
1171 1171
1172 1172 if stringutil.parsebool(options[b'pager']):
1173 1173 # ui.pager() expects 'internal-always-' prefix in this case
1174 1174 ui.pager(b'internal-always-' + cmd)
1175 1175 elif options[b'pager'] != b'auto':
1176 1176 for ui_ in uis:
1177 1177 ui_.disablepager()
1178 1178
1179 1179 # configs are fully loaded, set up the ui instances
1180 1180 for ui_ in uis:
1181 1181 extensions.populateui(ui_)
1182 1182
1183 1183 if options[b'version']:
1184 1184 return commands.version_(ui)
1185 1185 if options[b'help']:
1186 1186 return commands.help_(ui, cmd, command=cmd is not None)
1187 1187 elif not cmd:
1188 1188 return commands.help_(ui, b'shortlist')
1189 1189
1190 1190 repo = None
1191 1191 cmdpats = args[:]
1192 1192 assert func is not None # help out pytype
1193 1193 if not func.norepo:
1194 1194 # use the repo from the request only if we don't have -R
1195 1195 if not rpath and not cwd:
1196 1196 repo = req.repo
1197 1197
1198 1198 if repo:
1199 1199 # set the descriptors of the repo ui to those of ui
1200 1200 repo.ui.fin = ui.fin
1201 1201 repo.ui.fout = ui.fout
1202 1202 repo.ui.ferr = ui.ferr
1203 1203 repo.ui.fmsg = ui.fmsg
1204 1204 else:
1205 1205 try:
1206 1206 repo = hg.repository(
1207 1207 ui,
1208 1208 path=path,
1209 1209 presetupfuncs=req.prereposetups,
1210 1210 intents=func.intents,
1211 1211 )
1212 1212 if not repo.local():
1213 1213 raise error.InputError(
1214 1214 _(b"repository '%s' is not local") % path
1215 1215 )
1216 1216 repo.ui.setconfig(
1217 1217 b"bundle", b"mainreporoot", repo.root, b'repo'
1218 1218 )
1219 1219 except error.RequirementError:
1220 1220 raise
1221 1221 except error.RepoError:
1222 1222 if rpath: # invalid -R path
1223 1223 raise
1224 1224 if not func.optionalrepo:
1225 1225 if func.inferrepo and args and not path:
1226 1226 # try to infer -R from command args
1227 1227 repos = pycompat.maplist(cmdutil.findrepo, args)
1228 1228 guess = repos[0]
1229 1229 if guess and repos.count(guess) == len(repos):
1230 1230 req.args = [b'--repository', guess] + fullargs
1231 1231 req.earlyoptions[b'repository'] = guess
1232 1232 return _dispatch(req)
1233 1233 if not path:
1234 1234 raise error.InputError(
1235 1235 _(
1236 1236 b"no repository found in"
1237 1237 b" '%s' (.hg not found)"
1238 1238 )
1239 1239 % encoding.getcwd()
1240 1240 )
1241 1241 raise
1242 1242 if repo:
1243 1243 ui = repo.ui
1244 1244 if options[b'hidden']:
1245 1245 repo = repo.unfiltered()
1246 1246 args.insert(0, repo)
1247 1247 elif rpath:
1248 1248 ui.warn(_(b"warning: --repository ignored\n"))
1249 1249
1250 1250 msg = _formatargs(fullargs)
1251 1251 ui.log(b"command", b'%s\n', msg)
1252 1252 strcmdopt = pycompat.strkwargs(cmdoptions)
1253 1253 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
1254 1254 try:
1255 1255 return runcommand(
1256 1256 lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
1257 1257 )
1258 1258 finally:
1259 1259 if repo and repo != req.repo:
1260 1260 repo.close()
1261 1261
1262 1262
1263 1263 def _runcommand(ui, options, cmd, cmdfunc):
1264 1264 """Run a command function, possibly with profiling enabled."""
1265 1265 try:
1266 1266 with tracing.log("Running %s command" % cmd):
1267 1267 return cmdfunc()
1268 1268 except error.SignatureError:
1269 1269 raise error.CommandError(cmd, _(b'invalid arguments'))
1270 1270
1271 1271
1272 1272 def _exceptionwarning(ui):
1273 1273 """Produce a warning message for the current active exception"""
1274 1274
1275 1275 # For compatibility checking, we discard the portion of the hg
1276 1276 # version after the + on the assumption that if a "normal
1277 1277 # user" is running a build with a + in it the packager
1278 1278 # probably built from fairly close to a tag and anyone with a
1279 1279 # 'make local' copy of hg (where the version number can be out
1280 1280 # of date) will be clueful enough to notice the implausible
1281 1281 # version number and try updating.
1282 1282 ct = util.versiontuple(n=2)
1283 1283 worst = None, ct, b'', b''
1284 1284 if ui.config(b'ui', b'supportcontact') is None:
1285 1285 for name, mod in extensions.extensions():
1286 1286 # 'testedwith' should be bytes, but not all extensions are ported
1287 1287 # to py3 and we don't want UnicodeException because of that.
1288 1288 testedwith = stringutil.forcebytestr(
1289 1289 getattr(mod, 'testedwith', b'')
1290 1290 )
1291 1291 version = extensions.moduleversion(mod)
1292 1292 report = getattr(mod, 'buglink', _(b'the extension author.'))
1293 1293 if not testedwith.strip():
1294 1294 # We found an untested extension. It's likely the culprit.
1295 1295 worst = name, b'unknown', report, version
1296 1296 break
1297 1297
1298 1298 # Never blame on extensions bundled with Mercurial.
1299 1299 if extensions.ismoduleinternal(mod):
1300 1300 continue
1301 1301
1302 1302 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1303 1303 if ct in tested:
1304 1304 continue
1305 1305
1306 1306 lower = [t for t in tested if t < ct]
1307 1307 nearest = max(lower or tested)
1308 1308 if worst[0] is None or nearest < worst[1]:
1309 1309 worst = name, nearest, report, version
1310 1310 if worst[0] is not None:
1311 1311 name, testedwith, report, version = worst
1312 1312 if not isinstance(testedwith, (bytes, str)):
1313 1313 testedwith = b'.'.join(
1314 1314 [stringutil.forcebytestr(c) for c in testedwith]
1315 1315 )
1316 1316 extver = version or _(b"(version N/A)")
1317 1317 warning = _(
1318 1318 b'** Unknown exception encountered with '
1319 1319 b'possibly-broken third-party extension "%s" %s\n'
1320 1320 b'** which supports versions %s of Mercurial.\n'
1321 1321 b'** Please disable "%s" and try your action again.\n'
1322 1322 b'** If that fixes the bug please report it to %s\n'
1323 1323 ) % (name, extver, testedwith, name, stringutil.forcebytestr(report))
1324 1324 else:
1325 1325 bugtracker = ui.config(b'ui', b'supportcontact')
1326 1326 if bugtracker is None:
1327 1327 bugtracker = _(b"https://mercurial-scm.org/wiki/BugTracker")
1328 1328 warning = (
1329 1329 _(
1330 1330 b"** unknown exception encountered, "
1331 1331 b"please report by visiting\n** "
1332 1332 )
1333 1333 + bugtracker
1334 1334 + b'\n'
1335 1335 )
1336 1336 sysversion = pycompat.sysbytes(sys.version).replace(b'\n', b'')
1337 1337
1338 1338 def ext_with_ver(x):
1339 1339 ext = x[0]
1340 1340 ver = extensions.moduleversion(x[1])
1341 1341 if ver:
1342 1342 ext += b' ' + ver
1343 1343 return ext
1344 1344
1345 1345 warning += (
1346 1346 (_(b"** Python %s\n") % sysversion)
1347 1347 + (_(b"** Mercurial Distributed SCM (version %s)\n") % util.version())
1348 1348 + (
1349 1349 _(b"** Extensions loaded: %s\n")
1350 1350 % b", ".join(
1351 1351 [ext_with_ver(x) for x in sorted(extensions.extensions())]
1352 1352 )
1353 1353 )
1354 1354 )
1355 1355 return warning
1356 1356
1357 1357
1358 1358 def handlecommandexception(ui):
1359 1359 """Produce a warning message for broken commands
1360 1360
1361 1361 Called when handling an exception; the exception is reraised if
1362 1362 this function returns False, ignored otherwise.
1363 1363 """
1364 1364 warning = _exceptionwarning(ui)
1365 1365 ui.log(
1366 1366 b"commandexception",
1367 1367 b"%s\n%s\n",
1368 1368 warning,
1369 1369 pycompat.sysbytes(traceback.format_exc()),
1370 1370 )
1371 1371 ui.warn(warning)
1372 1372 return False # re-raise the exception
@@ -1,1204 +1,1313 b''
1 1 # This file is automatically @generated by Cargo.
2 2 # It is not intended for manual editing.
3 3 version = 3
4 4
5 5 [[package]]
6 name = "Inflector"
7 version = "0.11.4"
8 source = "registry+https://github.com/rust-lang/crates.io-index"
9 checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
10
11 [[package]]
6 12 name = "adler"
7 13 version = "0.2.3"
8 14 source = "registry+https://github.com/rust-lang/crates.io-index"
9 15 checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
10 16
11 17 [[package]]
12 18 name = "ahash"
13 19 version = "0.4.7"
14 20 source = "registry+https://github.com/rust-lang/crates.io-index"
15 21 checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
16 22
17 23 [[package]]
18 24 name = "aho-corasick"
19 25 version = "0.7.18"
20 26 source = "registry+https://github.com/rust-lang/crates.io-index"
21 27 checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
22 28 dependencies = [
23 29 "memchr",
24 30 ]
25 31
26 32 [[package]]
33 name = "aliasable"
34 version = "0.1.3"
35 source = "registry+https://github.com/rust-lang/crates.io-index"
36 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
37
38 [[package]]
27 39 name = "ansi_term"
28 40 version = "0.12.1"
29 41 source = "registry+https://github.com/rust-lang/crates.io-index"
30 42 checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
31 43 dependencies = [
32 44 "winapi",
33 45 ]
34 46
35 47 [[package]]
36 48 name = "atty"
37 49 version = "0.2.14"
38 50 source = "registry+https://github.com/rust-lang/crates.io-index"
39 51 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
40 52 dependencies = [
41 53 "hermit-abi",
42 54 "libc",
43 55 "winapi",
44 56 ]
45 57
46 58 [[package]]
47 59 name = "autocfg"
48 60 version = "1.0.1"
49 61 source = "registry+https://github.com/rust-lang/crates.io-index"
50 62 checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
51 63
52 64 [[package]]
53 65 name = "bitflags"
54 66 version = "1.3.2"
55 67 source = "registry+https://github.com/rust-lang/crates.io-index"
56 68 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
57 69
58 70 [[package]]
59 71 name = "bitmaps"
60 72 version = "2.1.0"
61 73 source = "registry+https://github.com/rust-lang/crates.io-index"
62 74 checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
63 75 dependencies = [
64 76 "typenum",
65 77 ]
66 78
67 79 [[package]]
68 80 name = "block-buffer"
69 81 version = "0.9.0"
70 82 source = "registry+https://github.com/rust-lang/crates.io-index"
71 83 checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
72 84 dependencies = [
73 85 "generic-array",
74 86 ]
75 87
76 88 [[package]]
77 89 name = "block-buffer"
78 90 version = "0.10.2"
79 91 source = "registry+https://github.com/rust-lang/crates.io-index"
80 92 checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
81 93 dependencies = [
82 94 "generic-array",
83 95 ]
84 96
85 97 [[package]]
86 98 name = "byteorder"
87 99 version = "1.4.3"
88 100 source = "registry+https://github.com/rust-lang/crates.io-index"
89 101 checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
90 102
91 103 [[package]]
92 104 name = "bytes-cast"
93 105 version = "0.2.0"
94 106 source = "registry+https://github.com/rust-lang/crates.io-index"
95 107 checksum = "0d434f9a4ecbe987e7ccfda7274b6f82ea52c9b63742565a65cb5e8ba0f2c452"
96 108 dependencies = [
97 109 "bytes-cast-derive",
98 110 ]
99 111
100 112 [[package]]
101 113 name = "bytes-cast-derive"
102 114 version = "0.1.0"
103 115 source = "registry+https://github.com/rust-lang/crates.io-index"
104 116 checksum = "cb936af9de38476664d6b58e529aff30d482e4ce1c5e150293d00730b0d81fdb"
105 117 dependencies = [
106 118 "proc-macro2",
107 119 "quote",
108 120 "syn",
109 121 ]
110 122
111 123 [[package]]
112 124 name = "cc"
113 125 version = "1.0.66"
114 126 source = "registry+https://github.com/rust-lang/crates.io-index"
115 127 checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
116 128 dependencies = [
117 129 "jobserver",
118 130 ]
119 131
120 132 [[package]]
121 133 name = "cfg-if"
122 134 version = "0.1.10"
123 135 source = "registry+https://github.com/rust-lang/crates.io-index"
124 136 checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
125 137
126 138 [[package]]
127 139 name = "cfg-if"
128 140 version = "1.0.0"
129 141 source = "registry+https://github.com/rust-lang/crates.io-index"
130 142 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
131 143
132 144 [[package]]
133 145 name = "chrono"
134 146 version = "0.4.19"
135 147 source = "registry+https://github.com/rust-lang/crates.io-index"
136 148 checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
137 149 dependencies = [
138 150 "libc",
139 151 "num-integer",
140 152 "num-traits",
141 153 "time",
142 154 "winapi",
143 155 ]
144 156
145 157 [[package]]
146 158 name = "clap"
147 159 version = "2.34.0"
148 160 source = "registry+https://github.com/rust-lang/crates.io-index"
149 161 checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
150 162 dependencies = [
151 163 "ansi_term",
152 164 "atty",
153 165 "bitflags",
154 166 "strsim",
155 167 "textwrap",
156 168 "unicode-width",
157 169 "vec_map",
158 170 ]
159 171
160 172 [[package]]
161 173 name = "const_fn"
162 174 version = "0.4.4"
163 175 source = "registry+https://github.com/rust-lang/crates.io-index"
164 176 checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
165 177
166 178 [[package]]
167 179 name = "convert_case"
168 180 version = "0.4.0"
169 181 source = "registry+https://github.com/rust-lang/crates.io-index"
170 182 checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
171 183
172 184 [[package]]
173 185 name = "cpufeatures"
174 186 version = "0.1.4"
175 187 source = "registry+https://github.com/rust-lang/crates.io-index"
176 188 checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8"
177 189 dependencies = [
178 190 "libc",
179 191 ]
180 192
181 193 [[package]]
182 194 name = "cpufeatures"
183 195 version = "0.2.1"
184 196 source = "registry+https://github.com/rust-lang/crates.io-index"
185 197 checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
186 198 dependencies = [
187 199 "libc",
188 200 ]
189 201
190 202 [[package]]
191 203 name = "cpython"
192 204 version = "0.7.0"
193 205 source = "registry+https://github.com/rust-lang/crates.io-index"
194 206 checksum = "b7d46ba8ace7f3a1d204ac5060a706d0a68de6b42eafb6a586cc08bebcffe664"
195 207 dependencies = [
196 208 "libc",
197 209 "num-traits",
198 210 "paste",
199 211 "python3-sys",
200 212 ]
201 213
202 214 [[package]]
203 215 name = "crc32fast"
204 216 version = "1.2.1"
205 217 source = "registry+https://github.com/rust-lang/crates.io-index"
206 218 checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
207 219 dependencies = [
208 220 "cfg-if 1.0.0",
209 221 ]
210 222
211 223 [[package]]
212 224 name = "crossbeam-channel"
225 version = "0.4.4"
226 source = "registry+https://github.com/rust-lang/crates.io-index"
227 checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
228 dependencies = [
229 "crossbeam-utils 0.7.2",
230 "maybe-uninit",
231 ]
232
233 [[package]]
234 name = "crossbeam-channel"
213 235 version = "0.5.2"
214 236 source = "registry+https://github.com/rust-lang/crates.io-index"
215 237 checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
216 238 dependencies = [
217 239 "cfg-if 1.0.0",
218 "crossbeam-utils",
240 "crossbeam-utils 0.8.1",
219 241 ]
220 242
221 243 [[package]]
222 244 name = "crossbeam-deque"
223 245 version = "0.8.0"
224 246 source = "registry+https://github.com/rust-lang/crates.io-index"
225 247 checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
226 248 dependencies = [
227 249 "cfg-if 1.0.0",
228 250 "crossbeam-epoch",
229 "crossbeam-utils",
251 "crossbeam-utils 0.8.1",
230 252 ]
231 253
232 254 [[package]]
233 255 name = "crossbeam-epoch"
234 256 version = "0.9.1"
235 257 source = "registry+https://github.com/rust-lang/crates.io-index"
236 258 checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
237 259 dependencies = [
238 260 "cfg-if 1.0.0",
239 261 "const_fn",
240 "crossbeam-utils",
262 "crossbeam-utils 0.8.1",
241 263 "lazy_static",
242 264 "memoffset",
243 265 "scopeguard",
244 266 ]
245 267
246 268 [[package]]
247 269 name = "crossbeam-utils"
270 version = "0.7.2"
271 source = "registry+https://github.com/rust-lang/crates.io-index"
272 checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
273 dependencies = [
274 "autocfg",
275 "cfg-if 0.1.10",
276 "lazy_static",
277 ]
278
279 [[package]]
280 name = "crossbeam-utils"
248 281 version = "0.8.1"
249 282 source = "registry+https://github.com/rust-lang/crates.io-index"
250 283 checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
251 284 dependencies = [
252 285 "autocfg",
253 286 "cfg-if 1.0.0",
254 287 "lazy_static",
255 288 ]
256 289
257 290 [[package]]
258 291 name = "crypto-common"
259 292 version = "0.1.2"
260 293 source = "registry+https://github.com/rust-lang/crates.io-index"
261 294 checksum = "a4600d695eb3f6ce1cd44e6e291adceb2cc3ab12f20a33777ecd0bf6eba34e06"
262 295 dependencies = [
263 296 "generic-array",
264 297 ]
265 298
266 299 [[package]]
267 300 name = "ctor"
268 301 version = "0.1.16"
269 302 source = "registry+https://github.com/rust-lang/crates.io-index"
270 303 checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484"
271 304 dependencies = [
272 305 "quote",
273 306 "syn",
274 307 ]
275 308
276 309 [[package]]
277 310 name = "derive_more"
278 311 version = "0.99.17"
279 312 source = "registry+https://github.com/rust-lang/crates.io-index"
280 313 checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
281 314 dependencies = [
282 315 "convert_case",
283 316 "proc-macro2",
284 317 "quote",
285 318 "rustc_version",
286 319 "syn",
287 320 ]
288 321
289 322 [[package]]
290 323 name = "diff"
291 324 version = "0.1.12"
292 325 source = "registry+https://github.com/rust-lang/crates.io-index"
293 326 checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
294 327
295 328 [[package]]
296 329 name = "digest"
297 330 version = "0.9.0"
298 331 source = "registry+https://github.com/rust-lang/crates.io-index"
299 332 checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
300 333 dependencies = [
301 334 "generic-array",
302 335 ]
303 336
304 337 [[package]]
305 338 name = "digest"
306 339 version = "0.10.2"
307 340 source = "registry+https://github.com/rust-lang/crates.io-index"
308 341 checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837"
309 342 dependencies = [
310 343 "block-buffer 0.10.2",
311 344 "crypto-common",
312 345 ]
313 346
314 347 [[package]]
315 348 name = "either"
316 349 version = "1.6.1"
317 350 source = "registry+https://github.com/rust-lang/crates.io-index"
318 351 checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
319 352
320 353 [[package]]
321 354 name = "env_logger"
322 355 version = "0.9.0"
323 356 source = "registry+https://github.com/rust-lang/crates.io-index"
324 357 checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
325 358 dependencies = [
326 359 "atty",
327 360 "humantime",
328 361 "log",
329 362 "regex",
330 363 "termcolor",
331 364 ]
332 365
333 366 [[package]]
334 367 name = "fastrand"
335 368 version = "1.7.0"
336 369 source = "registry+https://github.com/rust-lang/crates.io-index"
337 370 checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
338 371 dependencies = [
339 372 "instant",
340 373 ]
341 374
342 375 [[package]]
343 376 name = "flate2"
344 377 version = "1.0.22"
345 378 source = "registry+https://github.com/rust-lang/crates.io-index"
346 379 checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
347 380 dependencies = [
348 381 "cfg-if 1.0.0",
349 382 "crc32fast",
350 383 "libc",
351 384 "libz-sys",
352 385 "miniz_oxide",
353 386 ]
354 387
355 388 [[package]]
356 389 name = "format-bytes"
357 390 version = "0.3.0"
358 391 source = "registry+https://github.com/rust-lang/crates.io-index"
359 392 checksum = "48942366ef93975da38e175ac9e10068c6fc08ca9e85930d4f098f4d5b14c2fd"
360 393 dependencies = [
361 394 "format-bytes-macros",
362 395 ]
363 396
364 397 [[package]]
365 398 name = "format-bytes-macros"
366 399 version = "0.4.0"
367 400 source = "registry+https://github.com/rust-lang/crates.io-index"
368 401 checksum = "203aadebefcc73d12038296c228eabf830f99cba991b0032adf20e9fa6ce7e4f"
369 402 dependencies = [
370 403 "proc-macro2",
371 404 "quote",
372 405 "syn",
373 406 ]
374 407
375 408 [[package]]
376 409 name = "generic-array"
377 410 version = "0.14.4"
378 411 source = "registry+https://github.com/rust-lang/crates.io-index"
379 412 checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
380 413 dependencies = [
381 414 "typenum",
382 415 "version_check",
383 416 ]
384 417
385 418 [[package]]
386 419 name = "getrandom"
387 420 version = "0.1.15"
388 421 source = "registry+https://github.com/rust-lang/crates.io-index"
389 422 checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
390 423 dependencies = [
391 424 "cfg-if 0.1.10",
392 425 "libc",
393 426 "wasi 0.9.0+wasi-snapshot-preview1",
394 427 ]
395 428
396 429 [[package]]
397 430 name = "getrandom"
398 431 version = "0.2.4"
399 432 source = "registry+https://github.com/rust-lang/crates.io-index"
400 433 checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
401 434 dependencies = [
402 435 "cfg-if 1.0.0",
403 436 "libc",
404 437 "wasi 0.10.0+wasi-snapshot-preview1",
405 438 ]
406 439
407 440 [[package]]
408 441 name = "glob"
409 442 version = "0.3.0"
410 443 source = "registry+https://github.com/rust-lang/crates.io-index"
411 444 checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
412 445
413 446 [[package]]
414 447 name = "hashbrown"
415 448 version = "0.9.1"
416 449 source = "registry+https://github.com/rust-lang/crates.io-index"
417 450 checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
418 451 dependencies = [
419 452 "ahash",
420 453 "rayon",
421 454 ]
422 455
423 456 [[package]]
424 457 name = "hermit-abi"
425 458 version = "0.1.17"
426 459 source = "registry+https://github.com/rust-lang/crates.io-index"
427 460 checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
428 461 dependencies = [
429 462 "libc",
430 463 ]
431 464
432 465 [[package]]
433 466 name = "hex"
434 467 version = "0.4.3"
435 468 source = "registry+https://github.com/rust-lang/crates.io-index"
436 469 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
437 470
438 471 [[package]]
439 472 name = "hg-core"
440 473 version = "0.1.0"
441 474 dependencies = [
442 475 "bitflags",
443 476 "byteorder",
444 477 "bytes-cast",
445 478 "clap",
446 "crossbeam-channel",
479 "crossbeam-channel 0.4.4",
447 480 "derive_more",
448 481 "flate2",
449 482 "format-bytes",
450 483 "hashbrown",
451 484 "home",
452 485 "im-rc",
453 486 "itertools 0.10.3",
454 487 "lazy_static",
455 488 "libc",
456 489 "log",
457 490 "memmap2",
458 "micro-timer",
491 "micro-timer 0.3.1",
492 "ouroboros",
459 493 "pretty_assertions",
460 494 "rand 0.8.5",
461 495 "rand_distr",
462 496 "rand_pcg",
463 497 "rayon",
464 498 "regex",
465 499 "same-file",
466 500 "sha-1 0.10.0",
467 "stable_deref_trait",
468 501 "tempfile",
469 502 "twox-hash",
470 503 "zstd",
471 504 ]
472 505
473 506 [[package]]
474 507 name = "hg-cpython"
475 508 version = "0.1.0"
476 509 dependencies = [
477 510 "cpython",
478 "crossbeam-channel",
511 "crossbeam-channel 0.5.2",
479 512 "env_logger",
480 513 "hg-core",
481 514 "libc",
482 515 "log",
483 516 "stable_deref_trait",
484 517 "vcsgraph",
485 518 ]
486 519
487 520 [[package]]
488 521 name = "home"
489 522 version = "0.5.3"
490 523 source = "registry+https://github.com/rust-lang/crates.io-index"
491 524 checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
492 525 dependencies = [
493 526 "winapi",
494 527 ]
495 528
496 529 [[package]]
497 530 name = "humantime"
498 531 version = "2.1.0"
499 532 source = "registry+https://github.com/rust-lang/crates.io-index"
500 533 checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
501 534
502 535 [[package]]
503 536 name = "im-rc"
504 537 version = "15.0.0"
505 538 source = "registry+https://github.com/rust-lang/crates.io-index"
506 539 checksum = "3ca8957e71f04a205cb162508f9326aea04676c8dfd0711220190d6b83664f3f"
507 540 dependencies = [
508 541 "bitmaps",
509 542 "rand_core 0.5.1",
510 543 "rand_xoshiro",
511 544 "sized-chunks",
512 545 "typenum",
513 546 "version_check",
514 547 ]
515 548
516 549 [[package]]
517 550 name = "instant"
518 551 version = "0.1.12"
519 552 source = "registry+https://github.com/rust-lang/crates.io-index"
520 553 checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
521 554 dependencies = [
522 555 "cfg-if 1.0.0",
523 556 ]
524 557
525 558 [[package]]
526 559 name = "itertools"
527 560 version = "0.9.0"
528 561 source = "registry+https://github.com/rust-lang/crates.io-index"
529 562 checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
530 563 dependencies = [
531 564 "either",
532 565 ]
533 566
534 567 [[package]]
535 568 name = "itertools"
536 569 version = "0.10.3"
537 570 source = "registry+https://github.com/rust-lang/crates.io-index"
538 571 checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
539 572 dependencies = [
540 573 "either",
541 574 ]
542 575
543 576 [[package]]
544 577 name = "jobserver"
545 578 version = "0.1.21"
546 579 source = "registry+https://github.com/rust-lang/crates.io-index"
547 580 checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
548 581 dependencies = [
549 582 "libc",
550 583 ]
551 584
552 585 [[package]]
553 586 name = "lazy_static"
554 587 version = "1.4.0"
555 588 source = "registry+https://github.com/rust-lang/crates.io-index"
556 589 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
557 590
558 591 [[package]]
559 592 name = "libc"
560 593 version = "0.2.119"
561 594 source = "registry+https://github.com/rust-lang/crates.io-index"
562 595 checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
563 596
564 597 [[package]]
565 598 name = "libm"
566 599 version = "0.2.1"
567 600 source = "registry+https://github.com/rust-lang/crates.io-index"
568 601 checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
569 602
570 603 [[package]]
571 604 name = "libz-sys"
572 605 version = "1.1.2"
573 606 source = "registry+https://github.com/rust-lang/crates.io-index"
574 607 checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655"
575 608 dependencies = [
576 609 "cc",
577 610 "pkg-config",
578 611 "vcpkg",
579 612 ]
580 613
581 614 [[package]]
582 615 name = "log"
583 616 version = "0.4.14"
584 617 source = "registry+https://github.com/rust-lang/crates.io-index"
585 618 checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
586 619 dependencies = [
587 620 "cfg-if 1.0.0",
588 621 ]
589 622
590 623 [[package]]
624 name = "maybe-uninit"
625 version = "2.0.0"
626 source = "registry+https://github.com/rust-lang/crates.io-index"
627 checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
628
629 [[package]]
591 630 name = "memchr"
592 631 version = "2.4.1"
593 632 source = "registry+https://github.com/rust-lang/crates.io-index"
594 633 checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
595 634
596 635 [[package]]
597 636 name = "memmap2"
598 version = "0.5.3"
637 version = "0.4.0"
599 638 source = "registry+https://github.com/rust-lang/crates.io-index"
600 checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f"
639 checksum = "de5d3112c080d58ce560081baeaab7e1e864ca21795ddbf533d5b1842bb1ecf8"
601 640 dependencies = [
602 641 "libc",
603 642 "stable_deref_trait",
604 643 ]
605 644
606 645 [[package]]
607 646 name = "memoffset"
608 647 version = "0.6.1"
609 648 source = "registry+https://github.com/rust-lang/crates.io-index"
610 649 checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87"
611 650 dependencies = [
612 651 "autocfg",
613 652 ]
614 653
615 654 [[package]]
616 655 name = "micro-timer"
656 version = "0.3.1"
657 source = "registry+https://github.com/rust-lang/crates.io-index"
658 checksum = "2620153e1d903d26b72b89f0e9c48d8c4756cba941c185461dddc234980c298c"
659 dependencies = [
660 "micro-timer-macros 0.3.1",
661 "scopeguard",
662 ]
663
664 [[package]]
665 name = "micro-timer"
617 666 version = "0.4.0"
618 667 source = "registry+https://github.com/rust-lang/crates.io-index"
619 668 checksum = "5de32cb59a062672560d6f0842c4aa7714727457b9fe2daf8987d995a176a405"
620 669 dependencies = [
621 "micro-timer-macros",
670 "micro-timer-macros 0.4.0",
622 671 "scopeguard",
623 672 ]
624 673
625 674 [[package]]
626 675 name = "micro-timer-macros"
676 version = "0.3.1"
677 source = "registry+https://github.com/rust-lang/crates.io-index"
678 checksum = "e28a3473e6abd6e9aab36aaeef32ad22ae0bd34e79f376643594c2b152ec1c5d"
679 dependencies = [
680 "proc-macro2",
681 "quote",
682 "scopeguard",
683 "syn",
684 ]
685
686 [[package]]
687 name = "micro-timer-macros"
627 688 version = "0.4.0"
628 689 source = "registry+https://github.com/rust-lang/crates.io-index"
629 690 checksum = "cee948b94700125b52dfb68dd17c19f6326696c1df57f92c05ee857463c93ba1"
630 691 dependencies = [
631 692 "proc-macro2",
632 693 "quote",
633 694 "scopeguard",
634 695 "syn",
635 696 ]
636 697
637 698 [[package]]
638 699 name = "miniz_oxide"
639 700 version = "0.4.3"
640 701 source = "registry+https://github.com/rust-lang/crates.io-index"
641 702 checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
642 703 dependencies = [
643 704 "adler",
644 705 "autocfg",
645 706 ]
646 707
647 708 [[package]]
648 709 name = "num-integer"
649 710 version = "0.1.44"
650 711 source = "registry+https://github.com/rust-lang/crates.io-index"
651 712 checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
652 713 dependencies = [
653 714 "autocfg",
654 715 "num-traits",
655 716 ]
656 717
657 718 [[package]]
658 719 name = "num-traits"
659 720 version = "0.2.14"
660 721 source = "registry+https://github.com/rust-lang/crates.io-index"
661 722 checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
662 723 dependencies = [
663 724 "autocfg",
664 725 "libm",
665 726 ]
666 727
667 728 [[package]]
668 729 name = "num_cpus"
669 730 version = "1.13.0"
670 731 source = "registry+https://github.com/rust-lang/crates.io-index"
671 732 checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
672 733 dependencies = [
673 734 "hermit-abi",
674 735 "libc",
675 736 ]
676 737
677 738 [[package]]
678 739 name = "opaque-debug"
679 740 version = "0.3.0"
680 741 source = "registry+https://github.com/rust-lang/crates.io-index"
681 742 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
682 743
683 744 [[package]]
745 name = "ouroboros"
746 version = "0.15.0"
747 source = "registry+https://github.com/rust-lang/crates.io-index"
748 checksum = "9f31a3b678685b150cba82b702dcdc5e155893f63610cf388d30cd988d4ca2bf"
749 dependencies = [
750 "aliasable",
751 "ouroboros_macro",
752 "stable_deref_trait",
753 ]
754
755 [[package]]
756 name = "ouroboros_macro"
757 version = "0.15.0"
758 source = "registry+https://github.com/rust-lang/crates.io-index"
759 checksum = "084fd65d5dd8b3772edccb5ffd1e4b7eba43897ecd0f9401e330e8c542959408"
760 dependencies = [
761 "Inflector",
762 "proc-macro-error",
763 "proc-macro2",
764 "quote",
765 "syn",
766 ]
767
768 [[package]]
684 769 name = "output_vt100"
685 770 version = "0.1.2"
686 771 source = "registry+https://github.com/rust-lang/crates.io-index"
687 772 checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
688 773 dependencies = [
689 774 "winapi",
690 775 ]
691 776
692 777 [[package]]
693 778 name = "paste"
694 779 version = "1.0.5"
695 780 source = "registry+https://github.com/rust-lang/crates.io-index"
696 781 checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
697 782
698 783 [[package]]
699 784 name = "pkg-config"
700 785 version = "0.3.19"
701 786 source = "registry+https://github.com/rust-lang/crates.io-index"
702 787 checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
703 788
704 789 [[package]]
705 790 name = "ppv-lite86"
706 791 version = "0.2.10"
707 792 source = "registry+https://github.com/rust-lang/crates.io-index"
708 793 checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
709 794
710 795 [[package]]
711 796 name = "pretty_assertions"
712 797 version = "1.1.0"
713 798 source = "registry+https://github.com/rust-lang/crates.io-index"
714 799 checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50"
715 800 dependencies = [
716 801 "ansi_term",
717 802 "ctor",
718 803 "diff",
719 804 "output_vt100",
720 805 ]
721 806
722 807 [[package]]
808 name = "proc-macro-error"
809 version = "1.0.4"
810 source = "registry+https://github.com/rust-lang/crates.io-index"
811 checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
812 dependencies = [
813 "proc-macro-error-attr",
814 "proc-macro2",
815 "quote",
816 "syn",
817 "version_check",
818 ]
819
820 [[package]]
821 name = "proc-macro-error-attr"
822 version = "1.0.4"
823 source = "registry+https://github.com/rust-lang/crates.io-index"
824 checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
825 dependencies = [
826 "proc-macro2",
827 "quote",
828 "version_check",
829 ]
830
831 [[package]]
723 832 name = "proc-macro2"
724 833 version = "1.0.24"
725 834 source = "registry+https://github.com/rust-lang/crates.io-index"
726 835 checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
727 836 dependencies = [
728 837 "unicode-xid",
729 838 ]
730 839
731 840 [[package]]
732 841 name = "python3-sys"
733 842 version = "0.7.0"
734 843 source = "registry+https://github.com/rust-lang/crates.io-index"
735 844 checksum = "b18b32e64c103d5045f44644d7ddddd65336f7a0521f6fde673240a9ecceb77e"
736 845 dependencies = [
737 846 "libc",
738 847 "regex",
739 848 ]
740 849
741 850 [[package]]
742 851 name = "quote"
743 852 version = "1.0.7"
744 853 source = "registry+https://github.com/rust-lang/crates.io-index"
745 854 checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
746 855 dependencies = [
747 856 "proc-macro2",
748 857 ]
749 858
750 859 [[package]]
751 860 name = "rand"
752 861 version = "0.7.3"
753 862 source = "registry+https://github.com/rust-lang/crates.io-index"
754 863 checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
755 864 dependencies = [
756 865 "getrandom 0.1.15",
757 866 "libc",
758 867 "rand_chacha 0.2.2",
759 868 "rand_core 0.5.1",
760 869 "rand_hc",
761 870 ]
762 871
763 872 [[package]]
764 873 name = "rand"
765 874 version = "0.8.5"
766 875 source = "registry+https://github.com/rust-lang/crates.io-index"
767 876 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
768 877 dependencies = [
769 878 "libc",
770 879 "rand_chacha 0.3.1",
771 880 "rand_core 0.6.3",
772 881 ]
773 882
774 883 [[package]]
775 884 name = "rand_chacha"
776 885 version = "0.2.2"
777 886 source = "registry+https://github.com/rust-lang/crates.io-index"
778 887 checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
779 888 dependencies = [
780 889 "ppv-lite86",
781 890 "rand_core 0.5.1",
782 891 ]
783 892
784 893 [[package]]
785 894 name = "rand_chacha"
786 895 version = "0.3.1"
787 896 source = "registry+https://github.com/rust-lang/crates.io-index"
788 897 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
789 898 dependencies = [
790 899 "ppv-lite86",
791 900 "rand_core 0.6.3",
792 901 ]
793 902
794 903 [[package]]
795 904 name = "rand_core"
796 905 version = "0.5.1"
797 906 source = "registry+https://github.com/rust-lang/crates.io-index"
798 907 checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
799 908 dependencies = [
800 909 "getrandom 0.1.15",
801 910 ]
802 911
803 912 [[package]]
804 913 name = "rand_core"
805 914 version = "0.6.3"
806 915 source = "registry+https://github.com/rust-lang/crates.io-index"
807 916 checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
808 917 dependencies = [
809 918 "getrandom 0.2.4",
810 919 ]
811 920
812 921 [[package]]
813 922 name = "rand_distr"
814 923 version = "0.4.3"
815 924 source = "registry+https://github.com/rust-lang/crates.io-index"
816 925 checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
817 926 dependencies = [
818 927 "num-traits",
819 928 "rand 0.8.5",
820 929 ]
821 930
822 931 [[package]]
823 932 name = "rand_hc"
824 933 version = "0.2.0"
825 934 source = "registry+https://github.com/rust-lang/crates.io-index"
826 935 checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
827 936 dependencies = [
828 937 "rand_core 0.5.1",
829 938 ]
830 939
831 940 [[package]]
832 941 name = "rand_pcg"
833 942 version = "0.3.1"
834 943 source = "registry+https://github.com/rust-lang/crates.io-index"
835 944 checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
836 945 dependencies = [
837 946 "rand_core 0.6.3",
838 947 ]
839 948
840 949 [[package]]
841 950 name = "rand_xoshiro"
842 951 version = "0.4.0"
843 952 source = "registry+https://github.com/rust-lang/crates.io-index"
844 953 checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004"
845 954 dependencies = [
846 955 "rand_core 0.5.1",
847 956 ]
848 957
849 958 [[package]]
850 959 name = "rayon"
851 960 version = "1.5.1"
852 961 source = "registry+https://github.com/rust-lang/crates.io-index"
853 962 checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
854 963 dependencies = [
855 964 "autocfg",
856 965 "crossbeam-deque",
857 966 "either",
858 967 "rayon-core",
859 968 ]
860 969
861 970 [[package]]
862 971 name = "rayon-core"
863 972 version = "1.9.1"
864 973 source = "registry+https://github.com/rust-lang/crates.io-index"
865 974 checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
866 975 dependencies = [
867 "crossbeam-channel",
976 "crossbeam-channel 0.5.2",
868 977 "crossbeam-deque",
869 "crossbeam-utils",
978 "crossbeam-utils 0.8.1",
870 979 "lazy_static",
871 980 "num_cpus",
872 981 ]
873 982
874 983 [[package]]
875 984 name = "redox_syscall"
876 985 version = "0.2.11"
877 986 source = "registry+https://github.com/rust-lang/crates.io-index"
878 987 checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c"
879 988 dependencies = [
880 989 "bitflags",
881 990 ]
882 991
883 992 [[package]]
884 993 name = "regex"
885 994 version = "1.5.5"
886 995 source = "registry+https://github.com/rust-lang/crates.io-index"
887 996 checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
888 997 dependencies = [
889 998 "aho-corasick",
890 999 "memchr",
891 1000 "regex-syntax",
892 1001 ]
893 1002
894 1003 [[package]]
895 1004 name = "regex-syntax"
896 1005 version = "0.6.25"
897 1006 source = "registry+https://github.com/rust-lang/crates.io-index"
898 1007 checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
899 1008
900 1009 [[package]]
901 1010 name = "remove_dir_all"
902 1011 version = "0.5.3"
903 1012 source = "registry+https://github.com/rust-lang/crates.io-index"
904 1013 checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
905 1014 dependencies = [
906 1015 "winapi",
907 1016 ]
908 1017
909 1018 [[package]]
910 1019 name = "rhg"
911 1020 version = "0.1.0"
912 1021 dependencies = [
913 1022 "atty",
914 1023 "chrono",
915 1024 "clap",
916 1025 "derive_more",
917 1026 "env_logger",
918 1027 "format-bytes",
919 1028 "hg-core",
920 1029 "home",
921 1030 "lazy_static",
922 1031 "log",
923 "micro-timer",
1032 "micro-timer 0.4.0",
924 1033 "regex",
925 1034 "users",
926 1035 ]
927 1036
928 1037 [[package]]
929 1038 name = "rustc_version"
930 1039 version = "0.4.0"
931 1040 source = "registry+https://github.com/rust-lang/crates.io-index"
932 1041 checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
933 1042 dependencies = [
934 1043 "semver",
935 1044 ]
936 1045
937 1046 [[package]]
938 1047 name = "same-file"
939 1048 version = "1.0.6"
940 1049 source = "registry+https://github.com/rust-lang/crates.io-index"
941 1050 checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
942 1051 dependencies = [
943 1052 "winapi-util",
944 1053 ]
945 1054
946 1055 [[package]]
947 1056 name = "scopeguard"
948 1057 version = "1.1.0"
949 1058 source = "registry+https://github.com/rust-lang/crates.io-index"
950 1059 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
951 1060
952 1061 [[package]]
953 1062 name = "semver"
954 1063 version = "1.0.6"
955 1064 source = "registry+https://github.com/rust-lang/crates.io-index"
956 1065 checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d"
957 1066
958 1067 [[package]]
959 1068 name = "sha-1"
960 1069 version = "0.9.6"
961 1070 source = "registry+https://github.com/rust-lang/crates.io-index"
962 1071 checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16"
963 1072 dependencies = [
964 1073 "block-buffer 0.9.0",
965 1074 "cfg-if 1.0.0",
966 1075 "cpufeatures 0.1.4",
967 1076 "digest 0.9.0",
968 1077 "opaque-debug",
969 1078 ]
970 1079
971 1080 [[package]]
972 1081 name = "sha-1"
973 1082 version = "0.10.0"
974 1083 source = "registry+https://github.com/rust-lang/crates.io-index"
975 1084 checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
976 1085 dependencies = [
977 1086 "cfg-if 1.0.0",
978 1087 "cpufeatures 0.2.1",
979 1088 "digest 0.10.2",
980 1089 ]
981 1090
982 1091 [[package]]
983 1092 name = "sized-chunks"
984 1093 version = "0.6.2"
985 1094 source = "registry+https://github.com/rust-lang/crates.io-index"
986 1095 checksum = "1ec31ceca5644fa6d444cc77548b88b67f46db6f7c71683b0f9336e671830d2f"
987 1096 dependencies = [
988 1097 "bitmaps",
989 1098 "typenum",
990 1099 ]
991 1100
992 1101 [[package]]
993 1102 name = "stable_deref_trait"
994 1103 version = "1.2.0"
995 1104 source = "registry+https://github.com/rust-lang/crates.io-index"
996 1105 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
997 1106
998 1107 [[package]]
999 1108 name = "static_assertions"
1000 1109 version = "1.1.0"
1001 1110 source = "registry+https://github.com/rust-lang/crates.io-index"
1002 1111 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
1003 1112
1004 1113 [[package]]
1005 1114 name = "strsim"
1006 1115 version = "0.8.0"
1007 1116 source = "registry+https://github.com/rust-lang/crates.io-index"
1008 1117 checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
1009 1118
1010 1119 [[package]]
1011 1120 name = "syn"
1012 1121 version = "1.0.54"
1013 1122 source = "registry+https://github.com/rust-lang/crates.io-index"
1014 1123 checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
1015 1124 dependencies = [
1016 1125 "proc-macro2",
1017 1126 "quote",
1018 1127 "unicode-xid",
1019 1128 ]
1020 1129
1021 1130 [[package]]
1022 1131 name = "tempfile"
1023 1132 version = "3.3.0"
1024 1133 source = "registry+https://github.com/rust-lang/crates.io-index"
1025 1134 checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
1026 1135 dependencies = [
1027 1136 "cfg-if 1.0.0",
1028 1137 "fastrand",
1029 1138 "libc",
1030 1139 "redox_syscall",
1031 1140 "remove_dir_all",
1032 1141 "winapi",
1033 1142 ]
1034 1143
1035 1144 [[package]]
1036 1145 name = "termcolor"
1037 1146 version = "1.1.2"
1038 1147 source = "registry+https://github.com/rust-lang/crates.io-index"
1039 1148 checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
1040 1149 dependencies = [
1041 1150 "winapi-util",
1042 1151 ]
1043 1152
1044 1153 [[package]]
1045 1154 name = "textwrap"
1046 1155 version = "0.11.0"
1047 1156 source = "registry+https://github.com/rust-lang/crates.io-index"
1048 1157 checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
1049 1158 dependencies = [
1050 1159 "unicode-width",
1051 1160 ]
1052 1161
1053 1162 [[package]]
1054 1163 name = "time"
1055 1164 version = "0.1.44"
1056 1165 source = "registry+https://github.com/rust-lang/crates.io-index"
1057 1166 checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
1058 1167 dependencies = [
1059 1168 "libc",
1060 1169 "wasi 0.10.0+wasi-snapshot-preview1",
1061 1170 "winapi",
1062 1171 ]
1063 1172
1064 1173 [[package]]
1065 1174 name = "twox-hash"
1066 1175 version = "1.6.2"
1067 1176 source = "registry+https://github.com/rust-lang/crates.io-index"
1068 1177 checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0"
1069 1178 dependencies = [
1070 1179 "cfg-if 1.0.0",
1071 1180 "rand 0.8.5",
1072 1181 "static_assertions",
1073 1182 ]
1074 1183
1075 1184 [[package]]
1076 1185 name = "typenum"
1077 1186 version = "1.12.0"
1078 1187 source = "registry+https://github.com/rust-lang/crates.io-index"
1079 1188 checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
1080 1189
1081 1190 [[package]]
1082 1191 name = "unicode-width"
1083 1192 version = "0.1.9"
1084 1193 source = "registry+https://github.com/rust-lang/crates.io-index"
1085 1194 checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
1086 1195
1087 1196 [[package]]
1088 1197 name = "unicode-xid"
1089 1198 version = "0.2.1"
1090 1199 source = "registry+https://github.com/rust-lang/crates.io-index"
1091 1200 checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
1092 1201
1093 1202 [[package]]
1094 1203 name = "users"
1095 1204 version = "0.11.0"
1096 1205 source = "registry+https://github.com/rust-lang/crates.io-index"
1097 1206 checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
1098 1207 dependencies = [
1099 1208 "libc",
1100 1209 "log",
1101 1210 ]
1102 1211
1103 1212 [[package]]
1104 1213 name = "vcpkg"
1105 1214 version = "0.2.11"
1106 1215 source = "registry+https://github.com/rust-lang/crates.io-index"
1107 1216 checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
1108 1217
1109 1218 [[package]]
1110 1219 name = "vcsgraph"
1111 1220 version = "0.2.0"
1112 1221 source = "registry+https://github.com/rust-lang/crates.io-index"
1113 1222 checksum = "4cb68c231e2575f7503a7c19213875f9d4ec2e84e963a56ce3de4b6bee351ef7"
1114 1223 dependencies = [
1115 1224 "hex",
1116 1225 "rand 0.7.3",
1117 1226 "sha-1 0.9.6",
1118 1227 ]
1119 1228
1120 1229 [[package]]
1121 1230 name = "vec_map"
1122 1231 version = "0.8.2"
1123 1232 source = "registry+https://github.com/rust-lang/crates.io-index"
1124 1233 checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
1125 1234
1126 1235 [[package]]
1127 1236 name = "version_check"
1128 1237 version = "0.9.2"
1129 1238 source = "registry+https://github.com/rust-lang/crates.io-index"
1130 1239 checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
1131 1240
1132 1241 [[package]]
1133 1242 name = "wasi"
1134 1243 version = "0.9.0+wasi-snapshot-preview1"
1135 1244 source = "registry+https://github.com/rust-lang/crates.io-index"
1136 1245 checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
1137 1246
1138 1247 [[package]]
1139 1248 name = "wasi"
1140 1249 version = "0.10.0+wasi-snapshot-preview1"
1141 1250 source = "registry+https://github.com/rust-lang/crates.io-index"
1142 1251 checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
1143 1252
1144 1253 [[package]]
1145 1254 name = "winapi"
1146 1255 version = "0.3.9"
1147 1256 source = "registry+https://github.com/rust-lang/crates.io-index"
1148 1257 checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1149 1258 dependencies = [
1150 1259 "winapi-i686-pc-windows-gnu",
1151 1260 "winapi-x86_64-pc-windows-gnu",
1152 1261 ]
1153 1262
1154 1263 [[package]]
1155 1264 name = "winapi-i686-pc-windows-gnu"
1156 1265 version = "0.4.0"
1157 1266 source = "registry+https://github.com/rust-lang/crates.io-index"
1158 1267 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1159 1268
1160 1269 [[package]]
1161 1270 name = "winapi-util"
1162 1271 version = "0.1.5"
1163 1272 source = "registry+https://github.com/rust-lang/crates.io-index"
1164 1273 checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
1165 1274 dependencies = [
1166 1275 "winapi",
1167 1276 ]
1168 1277
1169 1278 [[package]]
1170 1279 name = "winapi-x86_64-pc-windows-gnu"
1171 1280 version = "0.4.0"
1172 1281 source = "registry+https://github.com/rust-lang/crates.io-index"
1173 1282 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1174 1283
1175 1284 [[package]]
1176 1285 name = "zstd"
1177 1286 version = "0.5.4+zstd.1.4.7"
1178 1287 source = "registry+https://github.com/rust-lang/crates.io-index"
1179 1288 checksum = "69996ebdb1ba8b1517f61387a883857818a66c8a295f487b1ffd8fd9d2c82910"
1180 1289 dependencies = [
1181 1290 "zstd-safe",
1182 1291 ]
1183 1292
1184 1293 [[package]]
1185 1294 name = "zstd-safe"
1186 1295 version = "2.0.6+zstd.1.4.7"
1187 1296 source = "registry+https://github.com/rust-lang/crates.io-index"
1188 1297 checksum = "98aa931fb69ecee256d44589d19754e61851ae4769bf963b385119b1cc37a49e"
1189 1298 dependencies = [
1190 1299 "libc",
1191 1300 "zstd-sys",
1192 1301 ]
1193 1302
1194 1303 [[package]]
1195 1304 name = "zstd-sys"
1196 1305 version = "1.4.18+zstd.1.4.7"
1197 1306 source = "registry+https://github.com/rust-lang/crates.io-index"
1198 1307 checksum = "a1e6e8778706838f43f771d80d37787cb2fe06dafe89dd3aebaf6721b9eaec81"
1199 1308 dependencies = [
1200 1309 "cc",
1201 1310 "glob",
1202 1311 "itertools 0.9.0",
1203 1312 "libc",
1204 1313 ]
@@ -1,48 +1,48 b''
1 1 [package]
2 2 name = "hg-core"
3 3 version = "0.1.0"
4 4 authors = ["Georges Racinet <gracinet@anybox.fr>"]
5 5 description = "Mercurial pure Rust core library, with no assumption on Python bindings (FFI)"
6 6 edition = "2018"
7 7
8 8 [lib]
9 9 name = "hg"
10 10
11 11 [dependencies]
12 12 bitflags = "1.3.2"
13 13 bytes-cast = "0.2.0"
14 14 byteorder = "1.4.3"
15 15 derive_more = "0.99.17"
16 16 hashbrown = { version = "0.9.1", features = ["rayon"] }
17 17 home = "0.5.3"
18 18 im-rc = "15.0.0"
19 19 itertools = "0.10.3"
20 20 lazy_static = "1.4.0"
21 libc = "0.2.119"
22 rand = "0.8.5"
21 libc = "0.2"
22 ouroboros = "0.15.0"
23 rand = "0.8.4"
23 24 rand_pcg = "0.3.1"
24 25 rand_distr = "0.4.3"
25 26 rayon = "1.5.1"
26 27 regex = "1.5.5"
27 28 sha-1 = "0.10.0"
28 29 twox-hash = "1.6.2"
29 30 same-file = "1.0.6"
30 stable_deref_trait = "1.2.0"
31 tempfile = "3.3.0"
32 crossbeam-channel = "0.5.2"
33 micro-timer = "0.4.0"
34 log = "0.4.14"
35 memmap2 = { version = "0.5.3", features = ["stable_deref_trait"] }
31 tempfile = "3.1.0"
32 crossbeam-channel = "0.4"
33 micro-timer = "0.3.0"
34 log = "0.4.8"
35 memmap2 = {version = "0.4", features = ["stable_deref_trait"]}
36 36 zstd = "0.5.3"
37 37 format-bytes = "0.3.0"
38 38
39 39 # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until
40 40 # we have a clearer view of which backend is the fastest.
41 41 [dependencies.flate2]
42 42 version = "1.0.22"
43 43 features = ["zlib"]
44 44 default-features = false
45 45
46 46 [dev-dependencies]
47 47 clap = "2.34.0"
48 48 pretty_assertions = "1.1.0"
@@ -1,151 +1,149 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Rust implementation of dirstate.status (dirstate.py).
9 9 //! It is currently missing a lot of functionality compared to the Python one
10 10 //! and will only be triggered in narrow cases.
11 11
12 12 use crate::dirstate::entry::TruncatedTimestamp;
13 13 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
14 14 use crate::{
15 15 utils::hg_path::{HgPath, HgPathError},
16 16 PatternError,
17 17 };
18 18
19 19 use std::{borrow::Cow, fmt};
20 20
21 21 /// Wrong type of file from a `BadMatch`
22 22 /// Note: a lot of those don't exist on all platforms.
23 23 #[derive(Debug, Copy, Clone)]
24 24 pub enum BadType {
25 25 CharacterDevice,
26 26 BlockDevice,
27 27 FIFO,
28 28 Socket,
29 29 Directory,
30 30 Unknown,
31 31 }
32 32
33 33 impl fmt::Display for BadType {
34 34 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35 35 f.write_str(match self {
36 36 BadType::CharacterDevice => "character device",
37 37 BadType::BlockDevice => "block device",
38 38 BadType::FIFO => "fifo",
39 39 BadType::Socket => "socket",
40 40 BadType::Directory => "directory",
41 41 BadType::Unknown => "unknown",
42 42 })
43 43 }
44 44 }
45 45
46 46 /// Was explicitly matched but cannot be found/accessed
47 47 #[derive(Debug, Copy, Clone)]
48 48 pub enum BadMatch {
49 49 OsError(i32),
50 50 BadType(BadType),
51 51 }
52 52
53 53 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add
54 54 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
55 55 pub type IgnoreFnType<'a> =
56 56 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
57 57
58 58 /// We have a good mix of owned (from directory traversal) and borrowed (from
59 59 /// the dirstate/explicit) paths, this comes up a lot.
60 60 pub type HgPathCow<'a> = Cow<'a, HgPath>;
61 61
62 62 #[derive(Debug, Copy, Clone)]
63 63 pub struct StatusOptions {
64 64 /// Whether we are on a filesystem with UNIX-like exec flags
65 65 pub check_exec: bool,
66 66 pub list_clean: bool,
67 67 pub list_unknown: bool,
68 68 pub list_ignored: bool,
69 69 /// Whether to populate `StatusPath::copy_source`
70 70 pub list_copies: bool,
71 71 /// Whether to collect traversed dirs for applying a callback later.
72 72 /// Used by `hg purge` for example.
73 73 pub collect_traversed_dirs: bool,
74 74 }
75 75
76 76 #[derive(Default)]
77 77 pub struct DirstateStatus<'a> {
78 78 /// The current time at the start of the `status()` algorithm, as measured
79 79 /// and possibly truncated by the filesystem.
80 80 pub filesystem_time_at_status_start: Option<TruncatedTimestamp>,
81 81
82 82 /// Tracked files whose contents have changed since the parent revision
83 83 pub modified: Vec<StatusPath<'a>>,
84 84
85 85 /// Newly-tracked files that were not present in the parent
86 86 pub added: Vec<StatusPath<'a>>,
87 87
88 88 /// Previously-tracked files that have been (re)moved with an hg command
89 89 pub removed: Vec<StatusPath<'a>>,
90 90
91 91 /// (Still) tracked files that are missing, (re)moved with an non-hg
92 92 /// command
93 93 pub deleted: Vec<StatusPath<'a>>,
94 94
95 95 /// Tracked files that are up to date with the parent.
96 96 /// Only pupulated if `StatusOptions::list_clean` is true.
97 97 pub clean: Vec<StatusPath<'a>>,
98 98
99 99 /// Files in the working directory that are ignored with `.hgignore`.
100 100 /// Only pupulated if `StatusOptions::list_ignored` is true.
101 101 pub ignored: Vec<StatusPath<'a>>,
102 102
103 103 /// Files in the working directory that are neither tracked nor ignored.
104 104 /// Only pupulated if `StatusOptions::list_unknown` is true.
105 105 pub unknown: Vec<StatusPath<'a>>,
106 106
107 107 /// Was explicitly matched but cannot be found/accessed
108 108 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
109 109
110 110 /// Either clean or modified, but we can’t tell from filesystem metadata
111 111 /// alone. The file contents need to be read and compared with that in
112 112 /// the parent.
113 113 pub unsure: Vec<StatusPath<'a>>,
114 114
115 115 /// Only filled if `collect_traversed_dirs` is `true`
116 116 pub traversed: Vec<HgPathCow<'a>>,
117 117
118 118 /// Whether `status()` made changed to the `DirstateMap` that should be
119 119 /// written back to disk
120 120 pub dirty: bool,
121 121 }
122 122
123 123 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
124 124 pub struct StatusPath<'a> {
125 125 pub path: HgPathCow<'a>,
126 126 pub copy_source: Option<HgPathCow<'a>>,
127 127 }
128 128
129 129 #[derive(Debug, derive_more::From)]
130 130 pub enum StatusError {
131 131 /// An invalid path that cannot be represented in Mercurial was found
132 132 Path(HgPathError),
133 133 /// An invalid "ignore" pattern was found
134 134 Pattern(PatternError),
135 135 /// Corrupted dirstate
136 136 DirstateV2ParseError(DirstateV2ParseError),
137 137 }
138 138
139 pub type StatusResult<T> = Result<T, StatusError>;
140
141 139 impl fmt::Display for StatusError {
142 140 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
143 141 match self {
144 142 StatusError::Path(error) => error.fmt(f),
145 143 StatusError::Pattern(error) => error.fmt(f),
146 144 StatusError::DirstateV2ParseError(_) => {
147 145 f.write_str("dirstate-v2 parse error")
148 146 }
149 147 }
150 148 }
151 149 }
@@ -1,1156 +1,1195 b''
1 1 use bytes_cast::BytesCast;
2 2 use micro_timer::timed;
3 3 use std::borrow::Cow;
4 4 use std::path::PathBuf;
5 5
6 6 use super::on_disk;
7 7 use super::on_disk::DirstateV2ParseError;
8 8 use super::owning::OwningDirstateMap;
9 9 use super::path_with_basename::WithBasename;
10 10 use crate::dirstate::parsers::pack_entry;
11 11 use crate::dirstate::parsers::packed_entry_size;
12 12 use crate::dirstate::parsers::parse_dirstate_entries;
13 13 use crate::dirstate::CopyMapIter;
14 14 use crate::dirstate::StateMapIter;
15 15 use crate::dirstate::TruncatedTimestamp;
16 16 use crate::dirstate::SIZE_FROM_OTHER_PARENT;
17 17 use crate::dirstate::SIZE_NON_NORMAL;
18 18 use crate::matchers::Matcher;
19 19 use crate::utils::hg_path::{HgPath, HgPathBuf};
20 20 use crate::DirstateEntry;
21 21 use crate::DirstateError;
22 22 use crate::DirstateParents;
23 23 use crate::DirstateStatus;
24 24 use crate::EntryState;
25 25 use crate::FastHashbrownMap as FastHashMap;
26 26 use crate::PatternFileWarning;
27 27 use crate::StatusError;
28 28 use crate::StatusOptions;
29 29
30 30 /// Append to an existing data file if the amount of unreachable data (not used
31 31 /// anymore) is less than this fraction of the total amount of existing data.
32 32 const ACCEPTABLE_UNREACHABLE_BYTES_RATIO: f32 = 0.5;
33 33
34 34 pub struct DirstateMap<'on_disk> {
35 35 /// Contents of the `.hg/dirstate` file
36 36 pub(super) on_disk: &'on_disk [u8],
37 37
38 38 pub(super) root: ChildNodes<'on_disk>,
39 39
40 40 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
41 41 pub(super) nodes_with_entry_count: u32,
42 42
43 43 /// Number of nodes anywhere in the tree that have
44 44 /// `.copy_source.is_some()`.
45 45 pub(super) nodes_with_copy_source_count: u32,
46 46
47 47 /// See on_disk::Header
48 48 pub(super) ignore_patterns_hash: on_disk::IgnorePatternsHash,
49 49
50 50 /// How many bytes of `on_disk` are not used anymore
51 51 pub(super) unreachable_bytes: u32,
52 52 }
53 53
54 54 /// Using a plain `HgPathBuf` of the full path from the repository root as a
55 55 /// map key would also work: all paths in a given map have the same parent
56 56 /// path, so comparing full paths gives the same result as comparing base
57 57 /// names. However `HashMap` would waste time always re-hashing the same
58 58 /// string prefix.
59 59 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
60 60
61 61 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
62 62 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
63 63 pub(super) enum BorrowedPath<'tree, 'on_disk> {
64 64 InMemory(&'tree HgPathBuf),
65 65 OnDisk(&'on_disk HgPath),
66 66 }
67 67
68 68 pub(super) enum ChildNodes<'on_disk> {
69 69 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
70 70 OnDisk(&'on_disk [on_disk::Node]),
71 71 }
72 72
73 73 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
74 74 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
75 75 OnDisk(&'on_disk [on_disk::Node]),
76 76 }
77 77
78 78 pub(super) enum NodeRef<'tree, 'on_disk> {
79 79 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
80 80 OnDisk(&'on_disk on_disk::Node),
81 81 }
82 82
83 83 impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> {
84 84 pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> {
85 85 match *self {
86 86 BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()),
87 87 BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk),
88 88 }
89 89 }
90 90 }
91 91
92 92 impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> {
93 93 type Target = HgPath;
94 94
95 95 fn deref(&self) -> &HgPath {
96 96 match *self {
97 97 BorrowedPath::InMemory(in_memory) => in_memory,
98 98 BorrowedPath::OnDisk(on_disk) => on_disk,
99 99 }
100 100 }
101 101 }
102 102
103 103 impl Default for ChildNodes<'_> {
104 104 fn default() -> Self {
105 105 ChildNodes::InMemory(Default::default())
106 106 }
107 107 }
108 108
109 109 impl<'on_disk> ChildNodes<'on_disk> {
110 110 pub(super) fn as_ref<'tree>(
111 111 &'tree self,
112 112 ) -> ChildNodesRef<'tree, 'on_disk> {
113 113 match self {
114 114 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
115 115 ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes),
116 116 }
117 117 }
118 118
119 119 pub(super) fn is_empty(&self) -> bool {
120 120 match self {
121 121 ChildNodes::InMemory(nodes) => nodes.is_empty(),
122 122 ChildNodes::OnDisk(nodes) => nodes.is_empty(),
123 123 }
124 124 }
125 125
126 126 fn make_mut(
127 127 &mut self,
128 128 on_disk: &'on_disk [u8],
129 129 unreachable_bytes: &mut u32,
130 130 ) -> Result<
131 131 &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>,
132 132 DirstateV2ParseError,
133 133 > {
134 134 match self {
135 135 ChildNodes::InMemory(nodes) => Ok(nodes),
136 136 ChildNodes::OnDisk(nodes) => {
137 137 *unreachable_bytes +=
138 138 std::mem::size_of_val::<[on_disk::Node]>(nodes) as u32;
139 139 let nodes = nodes
140 140 .iter()
141 141 .map(|node| {
142 142 Ok((
143 143 node.path(on_disk)?,
144 144 node.to_in_memory_node(on_disk)?,
145 145 ))
146 146 })
147 147 .collect::<Result<_, _>>()?;
148 148 *self = ChildNodes::InMemory(nodes);
149 149 match self {
150 150 ChildNodes::InMemory(nodes) => Ok(nodes),
151 151 ChildNodes::OnDisk(_) => unreachable!(),
152 152 }
153 153 }
154 154 }
155 155 }
156 156 }
157 157
158 158 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
159 159 pub(super) fn get(
160 160 &self,
161 161 base_name: &HgPath,
162 162 on_disk: &'on_disk [u8],
163 163 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
164 164 match self {
165 165 ChildNodesRef::InMemory(nodes) => Ok(nodes
166 166 .get_key_value(base_name)
167 167 .map(|(k, v)| NodeRef::InMemory(k, v))),
168 168 ChildNodesRef::OnDisk(nodes) => {
169 169 let mut parse_result = Ok(());
170 170 let search_result = nodes.binary_search_by(|node| {
171 171 match node.base_name(on_disk) {
172 172 Ok(node_base_name) => node_base_name.cmp(base_name),
173 173 Err(e) => {
174 174 parse_result = Err(e);
175 175 // Dummy comparison result, `search_result` won’t
176 176 // be used since `parse_result` is an error
177 177 std::cmp::Ordering::Equal
178 178 }
179 179 }
180 180 });
181 181 parse_result.map(|()| {
182 182 search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i]))
183 183 })
184 184 }
185 185 }
186 186 }
187 187
188 188 /// Iterate in undefined order
189 189 pub(super) fn iter(
190 190 &self,
191 191 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
192 192 match self {
193 193 ChildNodesRef::InMemory(nodes) => itertools::Either::Left(
194 194 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)),
195 195 ),
196 196 ChildNodesRef::OnDisk(nodes) => {
197 197 itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk))
198 198 }
199 199 }
200 200 }
201 201
202 202 /// Iterate in parallel in undefined order
203 203 pub(super) fn par_iter(
204 204 &self,
205 205 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
206 206 {
207 207 use rayon::prelude::*;
208 208 match self {
209 209 ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left(
210 210 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)),
211 211 ),
212 212 ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right(
213 213 nodes.par_iter().map(NodeRef::OnDisk),
214 214 ),
215 215 }
216 216 }
217 217
218 218 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
219 219 match self {
220 220 ChildNodesRef::InMemory(nodes) => {
221 221 let mut vec: Vec<_> = nodes
222 222 .iter()
223 223 .map(|(k, v)| NodeRef::InMemory(k, v))
224 224 .collect();
225 225 fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath {
226 226 match node {
227 227 NodeRef::InMemory(path, _node) => path.base_name(),
228 228 NodeRef::OnDisk(_) => unreachable!(),
229 229 }
230 230 }
231 231 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
232 232 // value: https://github.com/rust-lang/rust/issues/34162
233 233 vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b)));
234 234 vec
235 235 }
236 236 ChildNodesRef::OnDisk(nodes) => {
237 237 // Nodes on disk are already sorted
238 238 nodes.iter().map(NodeRef::OnDisk).collect()
239 239 }
240 240 }
241 241 }
242 242 }
243 243
244 244 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
245 245 pub(super) fn full_path(
246 246 &self,
247 247 on_disk: &'on_disk [u8],
248 248 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
249 249 match self {
250 250 NodeRef::InMemory(path, _node) => Ok(path.full_path()),
251 251 NodeRef::OnDisk(node) => node.full_path(on_disk),
252 252 }
253 253 }
254 254
255 255 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
256 256 /// HgPath>` detached from `'tree`
257 257 pub(super) fn full_path_borrowed(
258 258 &self,
259 259 on_disk: &'on_disk [u8],
260 260 ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> {
261 261 match self {
262 262 NodeRef::InMemory(path, _node) => match path.full_path() {
263 263 Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)),
264 264 Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)),
265 265 },
266 266 NodeRef::OnDisk(node) => {
267 267 Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?))
268 268 }
269 269 }
270 270 }
271 271
272 272 pub(super) fn base_name(
273 273 &self,
274 274 on_disk: &'on_disk [u8],
275 275 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
276 276 match self {
277 277 NodeRef::InMemory(path, _node) => Ok(path.base_name()),
278 278 NodeRef::OnDisk(node) => node.base_name(on_disk),
279 279 }
280 280 }
281 281
282 282 pub(super) fn children(
283 283 &self,
284 284 on_disk: &'on_disk [u8],
285 285 ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> {
286 286 match self {
287 287 NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()),
288 288 NodeRef::OnDisk(node) => {
289 289 Ok(ChildNodesRef::OnDisk(node.children(on_disk)?))
290 290 }
291 291 }
292 292 }
293 293
294 294 pub(super) fn has_copy_source(&self) -> bool {
295 295 match self {
296 296 NodeRef::InMemory(_path, node) => node.copy_source.is_some(),
297 297 NodeRef::OnDisk(node) => node.has_copy_source(),
298 298 }
299 299 }
300 300
301 301 pub(super) fn copy_source(
302 302 &self,
303 303 on_disk: &'on_disk [u8],
304 304 ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> {
305 305 match self {
306 306 NodeRef::InMemory(_path, node) => {
307 307 Ok(node.copy_source.as_ref().map(|s| &**s))
308 308 }
309 309 NodeRef::OnDisk(node) => node.copy_source(on_disk),
310 310 }
311 311 }
312 312 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
313 313 /// HgPath>` detached from `'tree`
314 314 pub(super) fn copy_source_borrowed(
315 315 &self,
316 316 on_disk: &'on_disk [u8],
317 317 ) -> Result<Option<BorrowedPath<'tree, 'on_disk>>, DirstateV2ParseError>
318 318 {
319 319 Ok(match self {
320 320 NodeRef::InMemory(_path, node) => {
321 321 node.copy_source.as_ref().map(|source| match source {
322 322 Cow::Borrowed(on_disk) => BorrowedPath::OnDisk(on_disk),
323 323 Cow::Owned(in_memory) => BorrowedPath::InMemory(in_memory),
324 324 })
325 325 }
326 326 NodeRef::OnDisk(node) => node
327 327 .copy_source(on_disk)?
328 328 .map(|source| BorrowedPath::OnDisk(source)),
329 329 })
330 330 }
331 331
332 332 pub(super) fn entry(
333 333 &self,
334 334 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
335 335 match self {
336 336 NodeRef::InMemory(_path, node) => {
337 337 Ok(node.data.as_entry().copied())
338 338 }
339 339 NodeRef::OnDisk(node) => node.entry(),
340 340 }
341 341 }
342 342
343 343 pub(super) fn state(
344 344 &self,
345 345 ) -> Result<Option<EntryState>, DirstateV2ParseError> {
346 346 Ok(self.entry()?.map(|e| e.state()))
347 347 }
348 348
349 349 pub(super) fn cached_directory_mtime(
350 350 &self,
351 351 ) -> Result<Option<TruncatedTimestamp>, DirstateV2ParseError> {
352 352 match self {
353 353 NodeRef::InMemory(_path, node) => Ok(match node.data {
354 354 NodeData::CachedDirectory { mtime } => Some(mtime),
355 355 _ => None,
356 356 }),
357 357 NodeRef::OnDisk(node) => node.cached_directory_mtime(),
358 358 }
359 359 }
360 360
361 361 pub(super) fn descendants_with_entry_count(&self) -> u32 {
362 362 match self {
363 363 NodeRef::InMemory(_path, node) => {
364 364 node.descendants_with_entry_count
365 365 }
366 366 NodeRef::OnDisk(node) => node.descendants_with_entry_count.get(),
367 367 }
368 368 }
369 369
370 370 pub(super) fn tracked_descendants_count(&self) -> u32 {
371 371 match self {
372 372 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
373 373 NodeRef::OnDisk(node) => node.tracked_descendants_count.get(),
374 374 }
375 375 }
376 376 }
377 377
378 378 /// Represents a file or a directory
379 379 #[derive(Default)]
380 380 pub(super) struct Node<'on_disk> {
381 381 pub(super) data: NodeData,
382 382
383 383 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
384 384
385 385 pub(super) children: ChildNodes<'on_disk>,
386 386
387 387 /// How many (non-inclusive) descendants of this node have an entry.
388 388 pub(super) descendants_with_entry_count: u32,
389 389
390 390 /// How many (non-inclusive) descendants of this node have an entry whose
391 391 /// state is "tracked".
392 392 pub(super) tracked_descendants_count: u32,
393 393 }
394 394
395 395 pub(super) enum NodeData {
396 396 Entry(DirstateEntry),
397 397 CachedDirectory { mtime: TruncatedTimestamp },
398 398 None,
399 399 }
400 400
401 401 impl Default for NodeData {
402 402 fn default() -> Self {
403 403 NodeData::None
404 404 }
405 405 }
406 406
407 407 impl NodeData {
408 408 fn has_entry(&self) -> bool {
409 409 match self {
410 410 NodeData::Entry(_) => true,
411 411 _ => false,
412 412 }
413 413 }
414 414
415 415 fn as_entry(&self) -> Option<&DirstateEntry> {
416 416 match self {
417 417 NodeData::Entry(entry) => Some(entry),
418 418 _ => None,
419 419 }
420 420 }
421 421 }
422 422
423 423 impl<'on_disk> DirstateMap<'on_disk> {
424 424 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
425 425 Self {
426 426 on_disk,
427 427 root: ChildNodes::default(),
428 428 nodes_with_entry_count: 0,
429 429 nodes_with_copy_source_count: 0,
430 430 ignore_patterns_hash: [0; on_disk::IGNORE_PATTERNS_HASH_LEN],
431 431 unreachable_bytes: 0,
432 432 }
433 433 }
434 434
435 435 #[timed]
436 436 pub fn new_v2(
437 437 on_disk: &'on_disk [u8],
438 438 data_size: usize,
439 439 metadata: &[u8],
440 440 ) -> Result<Self, DirstateError> {
441 441 if let Some(data) = on_disk.get(..data_size) {
442 442 Ok(on_disk::read(data, metadata)?)
443 443 } else {
444 444 Err(DirstateV2ParseError.into())
445 445 }
446 446 }
447 447
448 448 #[timed]
449 449 pub fn new_v1(
450 450 on_disk: &'on_disk [u8],
451 451 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
452 452 let mut map = Self::empty(on_disk);
453 453 if map.on_disk.is_empty() {
454 454 return Ok((map, None));
455 455 }
456 456
457 457 let parents = parse_dirstate_entries(
458 458 map.on_disk,
459 459 |path, entry, copy_source| {
460 460 let tracked = entry.state().is_tracked();
461 461 let node = Self::get_or_insert_node(
462 462 map.on_disk,
463 463 &mut map.unreachable_bytes,
464 464 &mut map.root,
465 465 path,
466 466 WithBasename::to_cow_borrowed,
467 467 |ancestor| {
468 468 if tracked {
469 469 ancestor.tracked_descendants_count += 1
470 470 }
471 471 ancestor.descendants_with_entry_count += 1
472 472 },
473 473 )?;
474 474 assert!(
475 475 !node.data.has_entry(),
476 476 "duplicate dirstate entry in read"
477 477 );
478 478 assert!(
479 479 node.copy_source.is_none(),
480 480 "duplicate dirstate entry in read"
481 481 );
482 482 node.data = NodeData::Entry(*entry);
483 483 node.copy_source = copy_source.map(Cow::Borrowed);
484 484 map.nodes_with_entry_count += 1;
485 485 if copy_source.is_some() {
486 486 map.nodes_with_copy_source_count += 1
487 487 }
488 488 Ok(())
489 489 },
490 490 )?;
491 491 let parents = Some(parents.clone());
492 492
493 493 Ok((map, parents))
494 494 }
495 495
496 496 /// Assuming dirstate-v2 format, returns whether the next write should
497 497 /// append to the existing data file that contains `self.on_disk` (true),
498 498 /// or create a new data file from scratch (false).
499 499 pub(super) fn write_should_append(&self) -> bool {
500 500 let ratio = self.unreachable_bytes as f32 / self.on_disk.len() as f32;
501 501 ratio < ACCEPTABLE_UNREACHABLE_BYTES_RATIO
502 502 }
503 503
504 504 fn get_node<'tree>(
505 505 &'tree self,
506 506 path: &HgPath,
507 507 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
508 508 let mut children = self.root.as_ref();
509 509 let mut components = path.components();
510 510 let mut component =
511 511 components.next().expect("expected at least one components");
512 512 loop {
513 513 if let Some(child) = children.get(component, self.on_disk)? {
514 514 if let Some(next_component) = components.next() {
515 515 component = next_component;
516 516 children = child.children(self.on_disk)?;
517 517 } else {
518 518 return Ok(Some(child));
519 519 }
520 520 } else {
521 521 return Ok(None);
522 522 }
523 523 }
524 524 }
525 525
526 526 /// Returns a mutable reference to the node at `path` if it exists
527 527 ///
528 528 /// This takes `root` instead of `&mut self` so that callers can mutate
529 529 /// other fields while the returned borrow is still valid
530 530 fn get_node_mut<'tree>(
531 531 on_disk: &'on_disk [u8],
532 532 unreachable_bytes: &mut u32,
533 533 root: &'tree mut ChildNodes<'on_disk>,
534 534 path: &HgPath,
535 535 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
536 536 let mut children = root;
537 537 let mut components = path.components();
538 538 let mut component =
539 539 components.next().expect("expected at least one components");
540 540 loop {
541 541 if let Some(child) = children
542 542 .make_mut(on_disk, unreachable_bytes)?
543 543 .get_mut(component)
544 544 {
545 545 if let Some(next_component) = components.next() {
546 546 component = next_component;
547 547 children = &mut child.children;
548 548 } else {
549 549 return Ok(Some(child));
550 550 }
551 551 } else {
552 552 return Ok(None);
553 553 }
554 554 }
555 555 }
556 556
557 557 pub(super) fn get_or_insert<'tree, 'path>(
558 558 &'tree mut self,
559 559 path: &HgPath,
560 560 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
561 561 Self::get_or_insert_node(
562 562 self.on_disk,
563 563 &mut self.unreachable_bytes,
564 564 &mut self.root,
565 565 path,
566 566 WithBasename::to_cow_owned,
567 567 |_| {},
568 568 )
569 569 }
570 570
571 571 fn get_or_insert_node<'tree, 'path>(
572 572 on_disk: &'on_disk [u8],
573 573 unreachable_bytes: &mut u32,
574 574 root: &'tree mut ChildNodes<'on_disk>,
575 575 path: &'path HgPath,
576 576 to_cow: impl Fn(
577 577 WithBasename<&'path HgPath>,
578 578 ) -> WithBasename<Cow<'on_disk, HgPath>>,
579 579 mut each_ancestor: impl FnMut(&mut Node),
580 580 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
581 581 let mut child_nodes = root;
582 582 let mut inclusive_ancestor_paths =
583 583 WithBasename::inclusive_ancestors_of(path);
584 584 let mut ancestor_path = inclusive_ancestor_paths
585 585 .next()
586 586 .expect("expected at least one inclusive ancestor");
587 587 loop {
588 588 let (_, child_node) = child_nodes
589 589 .make_mut(on_disk, unreachable_bytes)?
590 590 .raw_entry_mut()
591 591 .from_key(ancestor_path.base_name())
592 592 .or_insert_with(|| (to_cow(ancestor_path), Node::default()));
593 593 if let Some(next) = inclusive_ancestor_paths.next() {
594 594 each_ancestor(child_node);
595 595 ancestor_path = next;
596 596 child_nodes = &mut child_node.children;
597 597 } else {
598 598 return Ok(child_node);
599 599 }
600 600 }
601 601 }
602 602
603 603 fn add_or_remove_file(
604 604 &mut self,
605 605 path: &HgPath,
606 606 old_state: Option<EntryState>,
607 607 new_entry: DirstateEntry,
608 608 ) -> Result<(), DirstateV2ParseError> {
609 609 let had_entry = old_state.is_some();
610 610 let was_tracked = old_state.map_or(false, |s| s.is_tracked());
611 611 let tracked_count_increment =
612 612 match (was_tracked, new_entry.state().is_tracked()) {
613 613 (false, true) => 1,
614 614 (true, false) => -1,
615 615 _ => 0,
616 616 };
617 617
618 618 let node = Self::get_or_insert_node(
619 619 self.on_disk,
620 620 &mut self.unreachable_bytes,
621 621 &mut self.root,
622 622 path,
623 623 WithBasename::to_cow_owned,
624 624 |ancestor| {
625 625 if !had_entry {
626 626 ancestor.descendants_with_entry_count += 1;
627 627 }
628 628
629 629 // We can’t use `+= increment` because the counter is unsigned,
630 630 // and we want debug builds to detect accidental underflow
631 631 // through zero
632 632 match tracked_count_increment {
633 633 1 => ancestor.tracked_descendants_count += 1,
634 634 -1 => ancestor.tracked_descendants_count -= 1,
635 635 _ => {}
636 636 }
637 637 },
638 638 )?;
639 639 if !had_entry {
640 640 self.nodes_with_entry_count += 1
641 641 }
642 642 node.data = NodeData::Entry(new_entry);
643 643 Ok(())
644 644 }
645 645
646 646 fn iter_nodes<'tree>(
647 647 &'tree self,
648 648 ) -> impl Iterator<
649 649 Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>,
650 650 > + 'tree {
651 651 // Depth first tree traversal.
652 652 //
653 653 // If we could afford internal iteration and recursion,
654 654 // this would look like:
655 655 //
656 656 // ```
657 657 // fn traverse_children(
658 658 // children: &ChildNodes,
659 659 // each: &mut impl FnMut(&Node),
660 660 // ) {
661 661 // for child in children.values() {
662 662 // traverse_children(&child.children, each);
663 663 // each(child);
664 664 // }
665 665 // }
666 666 // ```
667 667 //
668 668 // However we want an external iterator and therefore can’t use the
669 669 // call stack. Use an explicit stack instead:
670 670 let mut stack = Vec::new();
671 671 let mut iter = self.root.as_ref().iter();
672 672 std::iter::from_fn(move || {
673 673 while let Some(child_node) = iter.next() {
674 674 let children = match child_node.children(self.on_disk) {
675 675 Ok(children) => children,
676 676 Err(error) => return Some(Err(error)),
677 677 };
678 678 // Pseudo-recursion
679 679 let new_iter = children.iter();
680 680 let old_iter = std::mem::replace(&mut iter, new_iter);
681 681 stack.push((child_node, old_iter));
682 682 }
683 683 // Found the end of a `children.iter()` iterator.
684 684 if let Some((child_node, next_iter)) = stack.pop() {
685 685 // "Return" from pseudo-recursion by restoring state from the
686 686 // explicit stack
687 687 iter = next_iter;
688 688
689 689 Some(Ok(child_node))
690 690 } else {
691 691 // Reached the bottom of the stack, we’re done
692 692 None
693 693 }
694 694 })
695 695 }
696 696
697 697 fn count_dropped_path(unreachable_bytes: &mut u32, path: &Cow<HgPath>) {
698 698 if let Cow::Borrowed(path) = path {
699 699 *unreachable_bytes += path.len() as u32
700 700 }
701 701 }
702 702 }
703 703
704 704 /// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s.
705 705 ///
706 706 /// The callback is only called for incoming `Ok` values. Errors are passed
707 707 /// through as-is. In order to let it use the `?` operator the callback is
708 708 /// expected to return a `Result` of `Option`, instead of an `Option` of
709 709 /// `Result`.
710 710 fn filter_map_results<'a, I, F, A, B, E>(
711 711 iter: I,
712 712 f: F,
713 713 ) -> impl Iterator<Item = Result<B, E>> + 'a
714 714 where
715 715 I: Iterator<Item = Result<A, E>> + 'a,
716 716 F: Fn(A) -> Result<Option<B>, E> + 'a,
717 717 {
718 718 iter.filter_map(move |result| match result {
719 719 Ok(node) => f(node).transpose(),
720 720 Err(e) => Some(Err(e)),
721 721 })
722 722 }
723 723
724 724 impl OwningDirstateMap {
725 725 pub fn clear(&mut self) {
726 let map = self.get_map_mut();
727 map.root = Default::default();
728 map.nodes_with_entry_count = 0;
729 map.nodes_with_copy_source_count = 0;
726 self.with_dmap_mut(|map| {
727 map.root = Default::default();
728 map.nodes_with_entry_count = 0;
729 map.nodes_with_copy_source_count = 0;
730 });
730 731 }
731 732
732 733 pub fn set_entry(
733 734 &mut self,
734 735 filename: &HgPath,
735 736 entry: DirstateEntry,
736 737 ) -> Result<(), DirstateV2ParseError> {
737 let map = self.get_map_mut();
738 map.get_or_insert(&filename)?.data = NodeData::Entry(entry);
739 Ok(())
738 self.with_dmap_mut(|map| {
739 map.get_or_insert(&filename)?.data = NodeData::Entry(entry);
740 Ok(())
741 })
740 742 }
741 743
742 744 pub fn add_file(
743 745 &mut self,
744 746 filename: &HgPath,
745 747 entry: DirstateEntry,
746 748 ) -> Result<(), DirstateError> {
747 749 let old_state = self.get(filename)?.map(|e| e.state());
748 let map = self.get_map_mut();
749 Ok(map.add_or_remove_file(filename, old_state, entry)?)
750 self.with_dmap_mut(|map| {
751 Ok(map.add_or_remove_file(filename, old_state, entry)?)
752 })
750 753 }
751 754
752 755 pub fn remove_file(
753 756 &mut self,
754 757 filename: &HgPath,
755 758 in_merge: bool,
756 759 ) -> Result<(), DirstateError> {
757 760 let old_entry_opt = self.get(filename)?;
758 761 let old_state = old_entry_opt.map(|e| e.state());
759 762 let mut size = 0;
760 763 if in_merge {
761 764 // XXX we should not be able to have 'm' state and 'FROM_P2' if not
762 765 // during a merge. So I (marmoute) am not sure we need the
763 766 // conditionnal at all. Adding double checking this with assert
764 767 // would be nice.
765 768 if let Some(old_entry) = old_entry_opt {
766 769 // backup the previous state
767 770 if old_entry.state() == EntryState::Merged {
768 771 size = SIZE_NON_NORMAL;
769 772 } else if old_entry.state() == EntryState::Normal
770 773 && old_entry.size() == SIZE_FROM_OTHER_PARENT
771 774 {
772 775 // other parent
773 776 size = SIZE_FROM_OTHER_PARENT;
774 777 }
775 778 }
776 779 }
777 780 if size == 0 {
778 781 self.copy_map_remove(filename)?;
779 782 }
780 let map = self.get_map_mut();
781 let entry = DirstateEntry::new_removed(size);
782 Ok(map.add_or_remove_file(filename, old_state, entry)?)
783 self.with_dmap_mut(|map| {
784 let entry = DirstateEntry::new_removed(size);
785 Ok(map.add_or_remove_file(filename, old_state, entry)?)
786 })
783 787 }
784 788
785 789 pub fn drop_entry_and_copy_source(
786 790 &mut self,
787 791 filename: &HgPath,
788 792 ) -> Result<(), DirstateError> {
789 793 let was_tracked = self
790 794 .get(filename)?
791 795 .map_or(false, |e| e.state().is_tracked());
792 let map = self.get_map_mut();
793 796 struct Dropped {
794 797 was_tracked: bool,
795 798 had_entry: bool,
796 799 had_copy_source: bool,
797 800 }
798 801
799 802 /// If this returns `Ok(Some((dropped, removed)))`, then
800 803 ///
801 804 /// * `dropped` is about the leaf node that was at `filename`
802 805 /// * `removed` is whether this particular level of recursion just
803 806 /// removed a node in `nodes`.
804 807 fn recur<'on_disk>(
805 808 on_disk: &'on_disk [u8],
806 809 unreachable_bytes: &mut u32,
807 810 nodes: &mut ChildNodes<'on_disk>,
808 811 path: &HgPath,
809 812 ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> {
810 813 let (first_path_component, rest_of_path) =
811 814 path.split_first_component();
812 815 let nodes = nodes.make_mut(on_disk, unreachable_bytes)?;
813 816 let node = if let Some(node) = nodes.get_mut(first_path_component)
814 817 {
815 818 node
816 819 } else {
817 820 return Ok(None);
818 821 };
819 822 let dropped;
820 823 if let Some(rest) = rest_of_path {
821 824 if let Some((d, removed)) = recur(
822 825 on_disk,
823 826 unreachable_bytes,
824 827 &mut node.children,
825 828 rest,
826 829 )? {
827 830 dropped = d;
828 831 if dropped.had_entry {
829 node.descendants_with_entry_count -= 1;
832 node.descendants_with_entry_count = node
833 .descendants_with_entry_count
834 .checked_sub(1)
835 .expect(
836 "descendants_with_entry_count should be >= 0",
837 );
830 838 }
831 839 if dropped.was_tracked {
832 node.tracked_descendants_count -= 1;
840 node.tracked_descendants_count = node
841 .tracked_descendants_count
842 .checked_sub(1)
843 .expect(
844 "tracked_descendants_count should be >= 0",
845 );
833 846 }
834 847
835 848 // Directory caches must be invalidated when removing a
836 849 // child node
837 850 if removed {
838 851 if let NodeData::CachedDirectory { .. } = &node.data {
839 852 node.data = NodeData::None
840 853 }
841 854 }
842 855 } else {
843 856 return Ok(None);
844 857 }
845 858 } else {
846 let had_entry = node.data.has_entry();
859 let entry = node.data.as_entry();
860 let was_tracked = entry.map_or(false, |entry| entry.tracked());
861 let had_entry = entry.is_some();
847 862 if had_entry {
848 863 node.data = NodeData::None
849 864 }
865 let mut had_copy_source = false;
850 866 if let Some(source) = &node.copy_source {
851 867 DirstateMap::count_dropped_path(unreachable_bytes, source);
868 had_copy_source = true;
852 869 node.copy_source = None
853 870 }
854 871 dropped = Dropped {
855 was_tracked: node
856 .data
857 .as_entry()
858 .map_or(false, |entry| entry.state().is_tracked()),
872 was_tracked,
859 873 had_entry,
860 had_copy_source: node.copy_source.take().is_some(),
874 had_copy_source,
861 875 };
862 876 }
863 877 // After recursion, for both leaf (rest_of_path is None) nodes and
864 878 // parent nodes, remove a node if it just became empty.
865 879 let remove = !node.data.has_entry()
866 880 && node.copy_source.is_none()
867 881 && node.children.is_empty();
868 882 if remove {
869 883 let (key, _) =
870 884 nodes.remove_entry(first_path_component).unwrap();
871 885 DirstateMap::count_dropped_path(
872 886 unreachable_bytes,
873 887 key.full_path(),
874 888 )
875 889 }
876 890 Ok(Some((dropped, remove)))
877 891 }
878 892
879 if let Some((dropped, _removed)) = recur(
880 map.on_disk,
881 &mut map.unreachable_bytes,
882 &mut map.root,
883 filename,
884 )? {
885 if dropped.had_entry {
886 map.nodes_with_entry_count -= 1
893 self.with_dmap_mut(|map| {
894 if let Some((dropped, _removed)) = recur(
895 map.on_disk,
896 &mut map.unreachable_bytes,
897 &mut map.root,
898 filename,
899 )? {
900 if dropped.had_entry {
901 map.nodes_with_entry_count = map
902 .nodes_with_entry_count
903 .checked_sub(1)
904 .expect("nodes_with_entry_count should be >= 0");
905 }
906 if dropped.had_copy_source {
907 map.nodes_with_copy_source_count = map
908 .nodes_with_copy_source_count
909 .checked_sub(1)
910 .expect("nodes_with_copy_source_count should be >= 0");
911 }
912 } else {
913 debug_assert!(!was_tracked);
887 914 }
888 if dropped.had_copy_source {
889 map.nodes_with_copy_source_count -= 1
890 }
891 } else {
892 debug_assert!(!was_tracked);
893 }
894 Ok(())
915 Ok(())
916 })
895 917 }
896 918
897 919 pub fn has_tracked_dir(
898 920 &mut self,
899 921 directory: &HgPath,
900 922 ) -> Result<bool, DirstateError> {
901 let map = self.get_map_mut();
902 if let Some(node) = map.get_node(directory)? {
903 // A node without a `DirstateEntry` was created to hold child
904 // nodes, and is therefore a directory.
905 let state = node.state()?;
906 Ok(state.is_none() && node.tracked_descendants_count() > 0)
907 } else {
908 Ok(false)
909 }
923 self.with_dmap_mut(|map| {
924 if let Some(node) = map.get_node(directory)? {
925 // A node without a `DirstateEntry` was created to hold child
926 // nodes, and is therefore a directory.
927 let state = node.state()?;
928 Ok(state.is_none() && node.tracked_descendants_count() > 0)
929 } else {
930 Ok(false)
931 }
932 })
910 933 }
911 934
912 935 pub fn has_dir(
913 936 &mut self,
914 937 directory: &HgPath,
915 938 ) -> Result<bool, DirstateError> {
916 let map = self.get_map_mut();
917 if let Some(node) = map.get_node(directory)? {
918 // A node without a `DirstateEntry` was created to hold child
919 // nodes, and is therefore a directory.
920 let state = node.state()?;
921 Ok(state.is_none() && node.descendants_with_entry_count() > 0)
922 } else {
923 Ok(false)
924 }
939 self.with_dmap_mut(|map| {
940 if let Some(node) = map.get_node(directory)? {
941 // A node without a `DirstateEntry` was created to hold child
942 // nodes, and is therefore a directory.
943 let state = node.state()?;
944 Ok(state.is_none() && node.descendants_with_entry_count() > 0)
945 } else {
946 Ok(false)
947 }
948 })
925 949 }
926 950
927 951 #[timed]
928 952 pub fn pack_v1(
929 953 &self,
930 954 parents: DirstateParents,
931 955 ) -> Result<Vec<u8>, DirstateError> {
932 956 let map = self.get_map();
933 957 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
934 958 // reallocations
935 959 let mut size = parents.as_bytes().len();
936 960 for node in map.iter_nodes() {
937 961 let node = node?;
938 962 if node.entry()?.is_some() {
939 963 size += packed_entry_size(
940 964 node.full_path(map.on_disk)?,
941 965 node.copy_source(map.on_disk)?,
942 966 );
943 967 }
944 968 }
945 969
946 970 let mut packed = Vec::with_capacity(size);
947 971 packed.extend(parents.as_bytes());
948 972
949 973 for node in map.iter_nodes() {
950 974 let node = node?;
951 975 if let Some(entry) = node.entry()? {
952 976 pack_entry(
953 977 node.full_path(map.on_disk)?,
954 978 &entry,
955 979 node.copy_source(map.on_disk)?,
956 980 &mut packed,
957 981 );
958 982 }
959 983 }
960 984 Ok(packed)
961 985 }
962 986
963 987 /// Returns new data and metadata together with whether that data should be
964 988 /// appended to the existing data file whose content is at
965 989 /// `map.on_disk` (true), instead of written to a new data file
966 990 /// (false).
967 991 #[timed]
968 992 pub fn pack_v2(
969 993 &self,
970 994 can_append: bool,
971 995 ) -> Result<(Vec<u8>, on_disk::TreeMetadata, bool), DirstateError> {
972 996 let map = self.get_map();
973 997 on_disk::write(map, can_append)
974 998 }
975 999
976 pub fn status<'a>(
977 &'a mut self,
978 matcher: &'a (dyn Matcher + Sync),
1000 /// `callback` allows the caller to process and do something with the
1001 /// results of the status. This is needed to do so efficiently (i.e.
1002 /// without cloning the `DirstateStatus` object with its paths) because
1003 /// we need to borrow from `Self`.
1004 pub fn with_status<R>(
1005 &mut self,
1006 matcher: &(dyn Matcher + Sync),
979 1007 root_dir: PathBuf,
980 1008 ignore_files: Vec<PathBuf>,
981 1009 options: StatusOptions,
982 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
983 {
984 let map = self.get_map_mut();
985 super::status::status(map, matcher, root_dir, ignore_files, options)
1010 callback: impl for<'r> FnOnce(
1011 Result<(DirstateStatus<'r>, Vec<PatternFileWarning>), StatusError>,
1012 ) -> R,
1013 ) -> R {
1014 self.with_dmap_mut(|map| {
1015 callback(super::status::status(
1016 map,
1017 matcher,
1018 root_dir,
1019 ignore_files,
1020 options,
1021 ))
1022 })
986 1023 }
987 1024
988 1025 pub fn copy_map_len(&self) -> usize {
989 1026 let map = self.get_map();
990 1027 map.nodes_with_copy_source_count as usize
991 1028 }
992 1029
993 1030 pub fn copy_map_iter(&self) -> CopyMapIter<'_> {
994 1031 let map = self.get_map();
995 1032 Box::new(filter_map_results(map.iter_nodes(), move |node| {
996 1033 Ok(if let Some(source) = node.copy_source(map.on_disk)? {
997 1034 Some((node.full_path(map.on_disk)?, source))
998 1035 } else {
999 1036 None
1000 1037 })
1001 1038 }))
1002 1039 }
1003 1040
1004 1041 pub fn copy_map_contains_key(
1005 1042 &self,
1006 1043 key: &HgPath,
1007 1044 ) -> Result<bool, DirstateV2ParseError> {
1008 1045 let map = self.get_map();
1009 1046 Ok(if let Some(node) = map.get_node(key)? {
1010 1047 node.has_copy_source()
1011 1048 } else {
1012 1049 false
1013 1050 })
1014 1051 }
1015 1052
1016 1053 pub fn copy_map_get(
1017 1054 &self,
1018 1055 key: &HgPath,
1019 1056 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
1020 1057 let map = self.get_map();
1021 1058 if let Some(node) = map.get_node(key)? {
1022 1059 if let Some(source) = node.copy_source(map.on_disk)? {
1023 1060 return Ok(Some(source));
1024 1061 }
1025 1062 }
1026 1063 Ok(None)
1027 1064 }
1028 1065
1029 1066 pub fn copy_map_remove(
1030 1067 &mut self,
1031 1068 key: &HgPath,
1032 1069 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1033 let map = self.get_map_mut();
1034 let count = &mut map.nodes_with_copy_source_count;
1035 let unreachable_bytes = &mut map.unreachable_bytes;
1036 Ok(DirstateMap::get_node_mut(
1037 map.on_disk,
1038 unreachable_bytes,
1039 &mut map.root,
1040 key,
1041 )?
1042 .and_then(|node| {
1043 if let Some(source) = &node.copy_source {
1044 *count -= 1;
1045 DirstateMap::count_dropped_path(unreachable_bytes, source);
1046 }
1047 node.copy_source.take().map(Cow::into_owned)
1048 }))
1070 self.with_dmap_mut(|map| {
1071 let count = &mut map.nodes_with_copy_source_count;
1072 let unreachable_bytes = &mut map.unreachable_bytes;
1073 Ok(DirstateMap::get_node_mut(
1074 map.on_disk,
1075 unreachable_bytes,
1076 &mut map.root,
1077 key,
1078 )?
1079 .and_then(|node| {
1080 if let Some(source) = &node.copy_source {
1081 *count -= 1;
1082 DirstateMap::count_dropped_path(unreachable_bytes, source);
1083 }
1084 node.copy_source.take().map(Cow::into_owned)
1085 }))
1086 })
1049 1087 }
1050 1088
1051 1089 pub fn copy_map_insert(
1052 1090 &mut self,
1053 1091 key: HgPathBuf,
1054 1092 value: HgPathBuf,
1055 1093 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1056 let map = self.get_map_mut();
1057 let node = DirstateMap::get_or_insert_node(
1058 map.on_disk,
1059 &mut map.unreachable_bytes,
1060 &mut map.root,
1061 &key,
1062 WithBasename::to_cow_owned,
1063 |_ancestor| {},
1064 )?;
1065 if node.copy_source.is_none() {
1066 map.nodes_with_copy_source_count += 1
1067 }
1068 Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
1094 self.with_dmap_mut(|map| {
1095 let node = DirstateMap::get_or_insert_node(
1096 map.on_disk,
1097 &mut map.unreachable_bytes,
1098 &mut map.root,
1099 &key,
1100 WithBasename::to_cow_owned,
1101 |_ancestor| {},
1102 )?;
1103 if node.copy_source.is_none() {
1104 map.nodes_with_copy_source_count += 1
1105 }
1106 Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
1107 })
1069 1108 }
1070 1109
1071 1110 pub fn len(&self) -> usize {
1072 1111 let map = self.get_map();
1073 1112 map.nodes_with_entry_count as usize
1074 1113 }
1075 1114
1076 1115 pub fn contains_key(
1077 1116 &self,
1078 1117 key: &HgPath,
1079 1118 ) -> Result<bool, DirstateV2ParseError> {
1080 1119 Ok(self.get(key)?.is_some())
1081 1120 }
1082 1121
1083 1122 pub fn get(
1084 1123 &self,
1085 1124 key: &HgPath,
1086 1125 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
1087 1126 let map = self.get_map();
1088 1127 Ok(if let Some(node) = map.get_node(key)? {
1089 1128 node.entry()?
1090 1129 } else {
1091 1130 None
1092 1131 })
1093 1132 }
1094 1133
1095 1134 pub fn iter(&self) -> StateMapIter<'_> {
1096 1135 let map = self.get_map();
1097 1136 Box::new(filter_map_results(map.iter_nodes(), move |node| {
1098 1137 Ok(if let Some(entry) = node.entry()? {
1099 1138 Some((node.full_path(map.on_disk)?, entry))
1100 1139 } else {
1101 1140 None
1102 1141 })
1103 1142 }))
1104 1143 }
1105 1144
1106 1145 pub fn iter_tracked_dirs(
1107 1146 &mut self,
1108 1147 ) -> Result<
1109 1148 Box<
1110 1149 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>>
1111 1150 + Send
1112 1151 + '_,
1113 1152 >,
1114 1153 DirstateError,
1115 1154 > {
1116 let map = self.get_map_mut();
1155 let map = self.get_map();
1117 1156 let on_disk = map.on_disk;
1118 1157 Ok(Box::new(filter_map_results(
1119 1158 map.iter_nodes(),
1120 1159 move |node| {
1121 1160 Ok(if node.tracked_descendants_count() > 0 {
1122 1161 Some(node.full_path(on_disk)?)
1123 1162 } else {
1124 1163 None
1125 1164 })
1126 1165 },
1127 1166 )))
1128 1167 }
1129 1168
1130 1169 pub fn debug_iter(
1131 1170 &self,
1132 1171 all: bool,
1133 1172 ) -> Box<
1134 1173 dyn Iterator<
1135 1174 Item = Result<
1136 1175 (&HgPath, (u8, i32, i32, i32)),
1137 1176 DirstateV2ParseError,
1138 1177 >,
1139 1178 > + Send
1140 1179 + '_,
1141 1180 > {
1142 1181 let map = self.get_map();
1143 1182 Box::new(filter_map_results(map.iter_nodes(), move |node| {
1144 1183 let debug_tuple = if let Some(entry) = node.entry()? {
1145 1184 entry.debug_tuple()
1146 1185 } else if !all {
1147 1186 return Ok(None);
1148 1187 } else if let Some(mtime) = node.cached_directory_mtime()? {
1149 1188 (b' ', 0, -1, mtime.truncated_seconds() as i32)
1150 1189 } else {
1151 1190 (b' ', 0, -1, -1)
1152 1191 };
1153 1192 Ok(Some((node.full_path(map.on_disk)?, debug_tuple)))
1154 1193 }))
1155 1194 }
1156 1195 }
@@ -1,105 +1,89 b''
1 use crate::{DirstateError, DirstateParents};
2
1 3 use super::dirstate_map::DirstateMap;
2 use stable_deref_trait::StableDeref;
3 4 use std::ops::Deref;
4 5
6 use ouroboros::self_referencing;
7
5 8 /// Keep a `DirstateMap<'on_disk>` next to the `on_disk` buffer that it
6 9 /// borrows.
7 ///
8 /// This is similar to [`OwningRef`] which is more limited because it
9 /// represents exactly one `&T` reference next to the value it borrows, as
10 /// opposed to a struct that may contain an arbitrary number of references in
11 /// arbitrarily-nested data structures.
12 ///
13 /// [`OwningRef`]: https://docs.rs/owning_ref/0.4.1/owning_ref/struct.OwningRef.html
10 #[self_referencing]
14 11 pub struct OwningDirstateMap {
15 /// Owned handle to a bytes buffer with a stable address.
16 ///
17 /// See <https://docs.rs/owning_ref/0.4.1/owning_ref/trait.StableAddress.html>.
18 12 on_disk: Box<dyn Deref<Target = [u8]> + Send>,
19
20 /// Pointer for `Box<DirstateMap<'on_disk>>`, typed-erased because the
21 /// language cannot represent a lifetime referencing a sibling field.
22 /// This is not quite a self-referencial struct (moving this struct is not
23 /// a problem as it doesn’t change the address of the bytes buffer owned
24 /// by `on_disk`) but touches similar borrow-checker limitations.
25 ptr: *mut (),
13 #[borrows(on_disk)]
14 #[covariant]
15 map: DirstateMap<'this>,
26 16 }
27 17
28 18 impl OwningDirstateMap {
29 19 pub fn new_empty<OnDisk>(on_disk: OnDisk) -> Self
30 20 where
31 OnDisk: Deref<Target = [u8]> + StableDeref + Send + 'static,
21 OnDisk: Deref<Target = [u8]> + Send + 'static,
32 22 {
33 23 let on_disk = Box::new(on_disk);
34 let bytes: &'_ [u8] = &on_disk;
35 let map = DirstateMap::empty(bytes);
36 24
37 // Like in `bytes` above, this `'_` lifetime parameter borrows from
38 // the bytes buffer owned by `on_disk`.
39 let ptr: *mut DirstateMap<'_> = Box::into_raw(Box::new(map));
40
41 // Erase the pointed type entirely in order to erase the lifetime.
42 let ptr: *mut () = ptr.cast();
43
44 Self { on_disk, ptr }
25 OwningDirstateMapBuilder {
26 on_disk,
27 map_builder: |bytes| DirstateMap::empty(&bytes),
28 }
29 .build()
45 30 }
46 31
47 pub fn get_pair_mut<'a>(
48 &'a mut self,
49 ) -> (&'a [u8], &'a mut DirstateMap<'a>) {
50 // SAFETY: We cast the type-erased pointer back to the same type it had
51 // in `new`, except with a different lifetime parameter. This time we
52 // connect the lifetime to that of `self`. This cast is valid because
53 // `self` owns the same `on_disk` whose buffer `DirstateMap`
54 // references. That buffer has a stable memory address because our
55 // `Self::new_empty` counstructor requires `StableDeref`.
56 let ptr: *mut DirstateMap<'a> = self.ptr.cast();
57 // SAFETY: we dereference that pointer, connecting the lifetime of the
58 // new `&mut` to that of `self`. This is valid because the
59 // raw pointer is to a boxed value, and `self` owns that box.
60 (&self.on_disk, unsafe { &mut *ptr })
61 }
32 pub fn new_v1<OnDisk>(
33 on_disk: OnDisk,
34 ) -> Result<(Self, DirstateParents), DirstateError>
35 where
36 OnDisk: Deref<Target = [u8]> + Send + 'static,
37 {
38 let on_disk = Box::new(on_disk);
39 let mut parents = DirstateParents::NULL;
62 40
63 pub fn get_map_mut<'a>(&'a mut self) -> &'a mut DirstateMap<'a> {
64 self.get_pair_mut().1
41 Ok((
42 OwningDirstateMapTryBuilder {
43 on_disk,
44 map_builder: |bytes| {
45 DirstateMap::new_v1(&bytes).map(|(dmap, p)| {
46 parents = p.unwrap_or(DirstateParents::NULL);
47 dmap
48 })
49 },
50 }
51 .try_build()?,
52 parents,
53 ))
65 54 }
66 55
67 pub fn get_map<'a>(&'a self) -> &'a DirstateMap<'a> {
68 // SAFETY: same reasoning as in `get_pair_mut` above.
69 let ptr: *mut DirstateMap<'a> = self.ptr.cast();
70 unsafe { &*ptr }
56 pub fn new_v2<OnDisk>(
57 on_disk: OnDisk,
58 data_size: usize,
59 metadata: &[u8],
60 ) -> Result<Self, DirstateError>
61 where
62 OnDisk: Deref<Target = [u8]> + Send + 'static,
63 {
64 let on_disk = Box::new(on_disk);
65
66 OwningDirstateMapTryBuilder {
67 on_disk,
68 map_builder: |bytes| {
69 DirstateMap::new_v2(&bytes, data_size, metadata)
70 },
71 }
72 .try_build()
71 73 }
72 74
73 pub fn on_disk<'a>(&'a self) -> &'a [u8] {
74 &self.on_disk
75 pub fn with_dmap_mut<R>(
76 &mut self,
77 f: impl FnOnce(&mut DirstateMap) -> R,
78 ) -> R {
79 self.with_map_mut(f)
80 }
81
82 pub fn get_map(&self) -> &DirstateMap {
83 self.borrow_map()
84 }
85
86 pub fn on_disk(&self) -> &[u8] {
87 self.borrow_on_disk()
75 88 }
76 89 }
77
78 impl Drop for OwningDirstateMap {
79 fn drop(&mut self) {
80 // Silence a "field is never read" warning, and demonstrate that this
81 // value is still alive.
82 let _: &Box<dyn Deref<Target = [u8]> + Send> = &self.on_disk;
83 // SAFETY: this cast is the same as in `get_mut`, and is valid for the
84 // same reason. `self.on_disk` still exists at this point, drop glue
85 // will drop it implicitly after this `drop` method returns.
86 let ptr: *mut DirstateMap<'_> = self.ptr.cast();
87 // SAFETY: `Box::from_raw` takes ownership of the box away from `self`.
88 // This is fine because drop glue does nothing for `*mut ()` and we’re
89 // in `drop`, so `get` and `get_mut` cannot be called again.
90 unsafe { drop(Box::from_raw(ptr)) }
91 }
92 }
93
94 fn _static_assert_is_send<T: Send>() {}
95
96 fn _static_assert_fields_are_send() {
97 _static_assert_is_send::<Box<DirstateMap<'_>>>();
98 }
99
100 // SAFETY: we don’t get this impl implicitly because `*mut (): !Send` because
101 // thread-safety of raw pointers is unknown in the general case. However this
102 // particular raw pointer represents a `Box<DirstateMap<'on_disk>>` that we
103 // own. Since that `Box` is `Send` as shown in above, it is sound to mark
104 // this struct as `Send` too.
105 unsafe impl Send for OwningDirstateMap {}
@@ -1,848 +1,849 b''
1 1 use crate::dirstate::entry::TruncatedTimestamp;
2 2 use crate::dirstate::status::IgnoreFnType;
3 3 use crate::dirstate::status::StatusPath;
4 4 use crate::dirstate_tree::dirstate_map::BorrowedPath;
5 5 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
6 6 use crate::dirstate_tree::dirstate_map::DirstateMap;
7 7 use crate::dirstate_tree::dirstate_map::NodeData;
8 8 use crate::dirstate_tree::dirstate_map::NodeRef;
9 9 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
10 10 use crate::matchers::get_ignore_function;
11 11 use crate::matchers::Matcher;
12 12 use crate::utils::files::get_bytes_from_os_string;
13 13 use crate::utils::files::get_path_from_bytes;
14 14 use crate::utils::hg_path::HgPath;
15 15 use crate::BadMatch;
16 16 use crate::DirstateStatus;
17 17 use crate::EntryState;
18 18 use crate::HgPathBuf;
19 19 use crate::HgPathCow;
20 20 use crate::PatternFileWarning;
21 21 use crate::StatusError;
22 22 use crate::StatusOptions;
23 23 use micro_timer::timed;
24 24 use rayon::prelude::*;
25 25 use sha1::{Digest, Sha1};
26 26 use std::borrow::Cow;
27 27 use std::io;
28 28 use std::path::Path;
29 29 use std::path::PathBuf;
30 30 use std::sync::Mutex;
31 31 use std::time::SystemTime;
32 32
33 33 /// Returns the status of the working directory compared to its parent
34 34 /// changeset.
35 35 ///
36 36 /// This algorithm is based on traversing the filesystem tree (`fs` in function
37 37 /// and variable names) and dirstate tree at the same time. The core of this
38 38 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
39 39 /// and its use of `itertools::merge_join_by`. When reaching a path that only
40 40 /// exists in one of the two trees, depending on information requested by
41 41 /// `options` we may need to traverse the remaining subtree.
42 42 #[timed]
43 pub fn status<'tree, 'on_disk: 'tree>(
44 dmap: &'tree mut DirstateMap<'on_disk>,
43 pub fn status<'dirstate>(
44 dmap: &'dirstate mut DirstateMap,
45 45 matcher: &(dyn Matcher + Sync),
46 46 root_dir: PathBuf,
47 47 ignore_files: Vec<PathBuf>,
48 48 options: StatusOptions,
49 ) -> Result<(DirstateStatus<'on_disk>, Vec<PatternFileWarning>), StatusError> {
49 ) -> Result<(DirstateStatus<'dirstate>, Vec<PatternFileWarning>), StatusError>
50 {
50 51 // Force the global rayon threadpool to not exceed 16 concurrent threads.
51 52 // This is a stop-gap measure until we figure out why using more than 16
52 53 // threads makes `status` slower for each additional thread.
53 54 // We use `ok()` in case the global threadpool has already been
54 55 // instantiated in `rhg` or some other caller.
55 56 // TODO find the underlying cause and fix it, then remove this.
56 57 rayon::ThreadPoolBuilder::new()
57 58 .num_threads(16)
58 59 .build_global()
59 60 .ok();
60 61
61 62 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
62 63 if options.list_ignored || options.list_unknown {
63 64 let mut hasher = Sha1::new();
64 65 let (ignore_fn, warnings) = get_ignore_function(
65 66 ignore_files,
66 67 &root_dir,
67 68 &mut |pattern_bytes| hasher.update(pattern_bytes),
68 69 )?;
69 70 let new_hash = *hasher.finalize().as_ref();
70 71 let changed = new_hash != dmap.ignore_patterns_hash;
71 72 dmap.ignore_patterns_hash = new_hash;
72 73 (ignore_fn, warnings, Some(changed))
73 74 } else {
74 75 (Box::new(|&_| true), vec![], None)
75 76 };
76 77
77 78 let filesystem_time_at_status_start =
78 79 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
79 80
80 81 // If the repository is under the current directory, prefer using a
81 82 // relative path, so the kernel needs to traverse fewer directory in every
82 83 // call to `read_dir` or `symlink_metadata`.
83 84 // This is effective in the common case where the current directory is the
84 85 // repository root.
85 86
86 87 // TODO: Better yet would be to use libc functions like `openat` and
87 88 // `fstatat` to remove such repeated traversals entirely, but the standard
88 89 // library does not provide APIs based on those.
89 90 // Maybe with a crate like https://crates.io/crates/openat instead?
90 91 let root_dir = if let Some(relative) = std::env::current_dir()
91 92 .ok()
92 93 .and_then(|cwd| root_dir.strip_prefix(cwd).ok())
93 94 {
94 95 relative
95 96 } else {
96 97 &root_dir
97 98 };
98 99
99 100 let outcome = DirstateStatus {
100 101 filesystem_time_at_status_start,
101 102 ..Default::default()
102 103 };
103 104 let common = StatusCommon {
104 105 dmap,
105 106 options,
106 107 matcher,
107 108 ignore_fn,
108 109 outcome: Mutex::new(outcome),
109 110 ignore_patterns_have_changed: patterns_changed,
110 111 new_cachable_directories: Default::default(),
111 112 outated_cached_directories: Default::default(),
112 113 filesystem_time_at_status_start,
113 114 };
114 115 let is_at_repo_root = true;
115 116 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
116 117 let has_ignored_ancestor = false;
117 118 let root_cached_mtime = None;
118 119 let root_dir_metadata = None;
119 120 // If the path we have for the repository root is a symlink, do follow it.
120 121 // (As opposed to symlinks within the working directory which are not
121 122 // followed, using `std::fs::symlink_metadata`.)
122 123 common.traverse_fs_directory_and_dirstate(
123 124 has_ignored_ancestor,
124 125 dmap.root.as_ref(),
125 126 hg_path,
126 127 &root_dir,
127 128 root_dir_metadata,
128 129 root_cached_mtime,
129 130 is_at_repo_root,
130 131 )?;
131 132 let mut outcome = common.outcome.into_inner().unwrap();
132 133 let new_cachable = common.new_cachable_directories.into_inner().unwrap();
133 134 let outdated = common.outated_cached_directories.into_inner().unwrap();
134 135
135 136 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
136 137 || !outdated.is_empty()
137 138 || !new_cachable.is_empty();
138 139
139 140 // Remove outdated mtimes before adding new mtimes, in case a given
140 141 // directory is both
141 142 for path in &outdated {
142 143 let node = dmap.get_or_insert(path)?;
143 144 if let NodeData::CachedDirectory { .. } = &node.data {
144 145 node.data = NodeData::None
145 146 }
146 147 }
147 148 for (path, mtime) in &new_cachable {
148 149 let node = dmap.get_or_insert(path)?;
149 150 match &node.data {
150 151 NodeData::Entry(_) => {} // Don’t overwrite an entry
151 152 NodeData::CachedDirectory { .. } | NodeData::None => {
152 153 node.data = NodeData::CachedDirectory { mtime: *mtime }
153 154 }
154 155 }
155 156 }
156 157
157 158 Ok((outcome, warnings))
158 159 }
159 160
160 161 /// Bag of random things needed by various parts of the algorithm. Reduces the
161 162 /// number of parameters passed to functions.
162 163 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
163 164 dmap: &'tree DirstateMap<'on_disk>,
164 165 options: StatusOptions,
165 166 matcher: &'a (dyn Matcher + Sync),
166 167 ignore_fn: IgnoreFnType<'a>,
167 168 outcome: Mutex<DirstateStatus<'on_disk>>,
168 169 new_cachable_directories:
169 170 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
170 171 outated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
171 172
172 173 /// Whether ignore files like `.hgignore` have changed since the previous
173 174 /// time a `status()` call wrote their hash to the dirstate. `None` means
174 175 /// we don’t know as this run doesn’t list either ignored or uknown files
175 176 /// and therefore isn’t reading `.hgignore`.
176 177 ignore_patterns_have_changed: Option<bool>,
177 178
178 179 /// The current time at the start of the `status()` algorithm, as measured
179 180 /// and possibly truncated by the filesystem.
180 181 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
181 182 }
182 183
183 184 enum Outcome {
184 185 Modified,
185 186 Added,
186 187 Removed,
187 188 Deleted,
188 189 Clean,
189 190 Ignored,
190 191 Unknown,
191 192 Unsure,
192 193 }
193 194
194 195 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
195 196 fn push_outcome(
196 197 &self,
197 198 which: Outcome,
198 199 dirstate_node: &NodeRef<'tree, 'on_disk>,
199 200 ) -> Result<(), DirstateV2ParseError> {
200 201 let path = dirstate_node
201 202 .full_path_borrowed(self.dmap.on_disk)?
202 203 .detach_from_tree();
203 204 let copy_source = if self.options.list_copies {
204 205 dirstate_node
205 206 .copy_source_borrowed(self.dmap.on_disk)?
206 207 .map(|source| source.detach_from_tree())
207 208 } else {
208 209 None
209 210 };
210 211 self.push_outcome_common(which, path, copy_source);
211 212 Ok(())
212 213 }
213 214
214 215 fn push_outcome_without_copy_source(
215 216 &self,
216 217 which: Outcome,
217 218 path: &BorrowedPath<'_, 'on_disk>,
218 219 ) {
219 220 self.push_outcome_common(which, path.detach_from_tree(), None)
220 221 }
221 222
222 223 fn push_outcome_common(
223 224 &self,
224 225 which: Outcome,
225 226 path: HgPathCow<'on_disk>,
226 227 copy_source: Option<HgPathCow<'on_disk>>,
227 228 ) {
228 229 let mut outcome = self.outcome.lock().unwrap();
229 230 let vec = match which {
230 231 Outcome::Modified => &mut outcome.modified,
231 232 Outcome::Added => &mut outcome.added,
232 233 Outcome::Removed => &mut outcome.removed,
233 234 Outcome::Deleted => &mut outcome.deleted,
234 235 Outcome::Clean => &mut outcome.clean,
235 236 Outcome::Ignored => &mut outcome.ignored,
236 237 Outcome::Unknown => &mut outcome.unknown,
237 238 Outcome::Unsure => &mut outcome.unsure,
238 239 };
239 240 vec.push(StatusPath { path, copy_source });
240 241 }
241 242
242 243 fn read_dir(
243 244 &self,
244 245 hg_path: &HgPath,
245 246 fs_path: &Path,
246 247 is_at_repo_root: bool,
247 248 ) -> Result<Vec<DirEntry>, ()> {
248 249 DirEntry::read_dir(fs_path, is_at_repo_root)
249 250 .map_err(|error| self.io_error(error, hg_path))
250 251 }
251 252
252 253 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
253 254 let errno = error.raw_os_error().expect("expected real OS error");
254 255 self.outcome
255 256 .lock()
256 257 .unwrap()
257 258 .bad
258 259 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
259 260 }
260 261
261 262 fn check_for_outdated_directory_cache(
262 263 &self,
263 264 dirstate_node: &NodeRef<'tree, 'on_disk>,
264 265 ) -> Result<(), DirstateV2ParseError> {
265 266 if self.ignore_patterns_have_changed == Some(true)
266 267 && dirstate_node.cached_directory_mtime()?.is_some()
267 268 {
268 269 self.outated_cached_directories.lock().unwrap().push(
269 270 dirstate_node
270 271 .full_path_borrowed(self.dmap.on_disk)?
271 272 .detach_from_tree(),
272 273 )
273 274 }
274 275 Ok(())
275 276 }
276 277
277 278 /// If this returns true, we can get accurate results by only using
278 279 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
279 280 /// need to call `read_dir`.
280 281 fn can_skip_fs_readdir(
281 282 &self,
282 283 directory_metadata: Option<&std::fs::Metadata>,
283 284 cached_directory_mtime: Option<TruncatedTimestamp>,
284 285 ) -> bool {
285 286 if !self.options.list_unknown && !self.options.list_ignored {
286 287 // All states that we care about listing have corresponding
287 288 // dirstate entries.
288 289 // This happens for example with `hg status -mard`.
289 290 return true;
290 291 }
291 292 if !self.options.list_ignored
292 293 && self.ignore_patterns_have_changed == Some(false)
293 294 {
294 295 if let Some(cached_mtime) = cached_directory_mtime {
295 296 // The dirstate contains a cached mtime for this directory, set
296 297 // by a previous run of the `status` algorithm which found this
297 298 // directory eligible for `read_dir` caching.
298 299 if let Some(meta) = directory_metadata {
299 300 if cached_mtime
300 301 .likely_equal_to_mtime_of(meta)
301 302 .unwrap_or(false)
302 303 {
303 304 // The mtime of that directory has not changed
304 305 // since then, which means that the results of
305 306 // `read_dir` should also be unchanged.
306 307 return true;
307 308 }
308 309 }
309 310 }
310 311 }
311 312 false
312 313 }
313 314
314 315 /// Returns whether all child entries of the filesystem directory have a
315 316 /// corresponding dirstate node or are ignored.
316 317 fn traverse_fs_directory_and_dirstate(
317 318 &self,
318 319 has_ignored_ancestor: bool,
319 320 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
320 321 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
321 322 directory_fs_path: &Path,
322 323 directory_metadata: Option<&std::fs::Metadata>,
323 324 cached_directory_mtime: Option<TruncatedTimestamp>,
324 325 is_at_repo_root: bool,
325 326 ) -> Result<bool, DirstateV2ParseError> {
326 327 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
327 328 {
328 329 dirstate_nodes
329 330 .par_iter()
330 331 .map(|dirstate_node| {
331 332 let fs_path = directory_fs_path.join(get_path_from_bytes(
332 333 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
333 334 ));
334 335 match std::fs::symlink_metadata(&fs_path) {
335 336 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
336 337 &fs_path,
337 338 &fs_metadata,
338 339 dirstate_node,
339 340 has_ignored_ancestor,
340 341 ),
341 342 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
342 343 self.traverse_dirstate_only(dirstate_node)
343 344 }
344 345 Err(error) => {
345 346 let hg_path =
346 347 dirstate_node.full_path(self.dmap.on_disk)?;
347 348 Ok(self.io_error(error, hg_path))
348 349 }
349 350 }
350 351 })
351 352 .collect::<Result<_, _>>()?;
352 353
353 354 // We don’t know, so conservatively say this isn’t the case
354 355 let children_all_have_dirstate_node_or_are_ignored = false;
355 356
356 357 return Ok(children_all_have_dirstate_node_or_are_ignored);
357 358 }
358 359
359 360 let mut fs_entries = if let Ok(entries) = self.read_dir(
360 361 directory_hg_path,
361 362 directory_fs_path,
362 363 is_at_repo_root,
363 364 ) {
364 365 entries
365 366 } else {
366 367 // Treat an unreadable directory (typically because of insufficient
367 368 // permissions) like an empty directory. `self.read_dir` has
368 369 // already called `self.io_error` so a warning will be emitted.
369 370 Vec::new()
370 371 };
371 372
372 373 // `merge_join_by` requires both its input iterators to be sorted:
373 374
374 375 let dirstate_nodes = dirstate_nodes.sorted();
375 376 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
376 377 // https://github.com/rust-lang/rust/issues/34162
377 378 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
378 379
379 380 // Propagate here any error that would happen inside the comparison
380 381 // callback below
381 382 for dirstate_node in &dirstate_nodes {
382 383 dirstate_node.base_name(self.dmap.on_disk)?;
383 384 }
384 385 itertools::merge_join_by(
385 386 dirstate_nodes,
386 387 &fs_entries,
387 388 |dirstate_node, fs_entry| {
388 389 // This `unwrap` never panics because we already propagated
389 390 // those errors above
390 391 dirstate_node
391 392 .base_name(self.dmap.on_disk)
392 393 .unwrap()
393 394 .cmp(&fs_entry.base_name)
394 395 },
395 396 )
396 397 .par_bridge()
397 398 .map(|pair| {
398 399 use itertools::EitherOrBoth::*;
399 400 let has_dirstate_node_or_is_ignored;
400 401 match pair {
401 402 Both(dirstate_node, fs_entry) => {
402 403 self.traverse_fs_and_dirstate(
403 404 &fs_entry.full_path,
404 405 &fs_entry.metadata,
405 406 dirstate_node,
406 407 has_ignored_ancestor,
407 408 )?;
408 409 has_dirstate_node_or_is_ignored = true
409 410 }
410 411 Left(dirstate_node) => {
411 412 self.traverse_dirstate_only(dirstate_node)?;
412 413 has_dirstate_node_or_is_ignored = true;
413 414 }
414 415 Right(fs_entry) => {
415 416 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
416 417 has_ignored_ancestor,
417 418 directory_hg_path,
418 419 fs_entry,
419 420 )
420 421 }
421 422 }
422 423 Ok(has_dirstate_node_or_is_ignored)
423 424 })
424 425 .try_reduce(|| true, |a, b| Ok(a && b))
425 426 }
426 427
427 428 fn traverse_fs_and_dirstate(
428 429 &self,
429 430 fs_path: &Path,
430 431 fs_metadata: &std::fs::Metadata,
431 432 dirstate_node: NodeRef<'tree, 'on_disk>,
432 433 has_ignored_ancestor: bool,
433 434 ) -> Result<(), DirstateV2ParseError> {
434 435 self.check_for_outdated_directory_cache(&dirstate_node)?;
435 436 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
436 437 let file_type = fs_metadata.file_type();
437 438 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
438 439 if !file_or_symlink {
439 440 // If we previously had a file here, it was removed (with
440 441 // `hg rm` or similar) or deleted before it could be
441 442 // replaced by a directory or something else.
442 443 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
443 444 }
444 445 if file_type.is_dir() {
445 446 if self.options.collect_traversed_dirs {
446 447 self.outcome
447 448 .lock()
448 449 .unwrap()
449 450 .traversed
450 451 .push(hg_path.detach_from_tree())
451 452 }
452 453 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
453 454 let is_at_repo_root = false;
454 455 let children_all_have_dirstate_node_or_are_ignored = self
455 456 .traverse_fs_directory_and_dirstate(
456 457 is_ignored,
457 458 dirstate_node.children(self.dmap.on_disk)?,
458 459 hg_path,
459 460 fs_path,
460 461 Some(fs_metadata),
461 462 dirstate_node.cached_directory_mtime()?,
462 463 is_at_repo_root,
463 464 )?;
464 465 self.maybe_save_directory_mtime(
465 466 children_all_have_dirstate_node_or_are_ignored,
466 467 fs_metadata,
467 468 dirstate_node,
468 469 )?
469 470 } else {
470 471 if file_or_symlink && self.matcher.matches(hg_path) {
471 472 if let Some(state) = dirstate_node.state()? {
472 473 match state {
473 474 EntryState::Added => {
474 475 self.push_outcome(Outcome::Added, &dirstate_node)?
475 476 }
476 477 EntryState::Removed => self
477 478 .push_outcome(Outcome::Removed, &dirstate_node)?,
478 479 EntryState::Merged => self
479 480 .push_outcome(Outcome::Modified, &dirstate_node)?,
480 481 EntryState::Normal => self
481 482 .handle_normal_file(&dirstate_node, fs_metadata)?,
482 483 }
483 484 } else {
484 485 // `node.entry.is_none()` indicates a "directory"
485 486 // node, but the filesystem has a file
486 487 self.mark_unknown_or_ignored(
487 488 has_ignored_ancestor,
488 489 hg_path,
489 490 );
490 491 }
491 492 }
492 493
493 494 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
494 495 {
495 496 self.traverse_dirstate_only(child_node)?
496 497 }
497 498 }
498 499 Ok(())
499 500 }
500 501
501 502 fn maybe_save_directory_mtime(
502 503 &self,
503 504 children_all_have_dirstate_node_or_are_ignored: bool,
504 505 directory_metadata: &std::fs::Metadata,
505 506 dirstate_node: NodeRef<'tree, 'on_disk>,
506 507 ) -> Result<(), DirstateV2ParseError> {
507 508 if !children_all_have_dirstate_node_or_are_ignored {
508 509 return Ok(());
509 510 }
510 511 // All filesystem directory entries from `read_dir` have a
511 512 // corresponding node in the dirstate, so we can reconstitute the
512 513 // names of those entries without calling `read_dir` again.
513 514
514 515 // TODO: use let-else here and below when available:
515 516 // https://github.com/rust-lang/rust/issues/87335
516 517 let status_start = if let Some(status_start) =
517 518 &self.filesystem_time_at_status_start
518 519 {
519 520 status_start
520 521 } else {
521 522 return Ok(());
522 523 };
523 524
524 525 // Although the Rust standard library’s `SystemTime` type
525 526 // has nanosecond precision, the times reported for a
526 527 // directory’s (or file’s) modified time may have lower
527 528 // resolution based on the filesystem (for example ext3
528 529 // only stores integer seconds), kernel (see
529 530 // https://stackoverflow.com/a/14393315/1162888), etc.
530 531 let directory_mtime = if let Ok(option) =
531 532 TruncatedTimestamp::for_reliable_mtime_of(
532 533 directory_metadata,
533 534 status_start,
534 535 ) {
535 536 if let Some(directory_mtime) = option {
536 537 directory_mtime
537 538 } else {
538 539 // The directory was modified too recently,
539 540 // don’t cache its `read_dir` results.
540 541 //
541 542 // 1. A change to this directory (direct child was
542 543 // added or removed) cause its mtime to be set
543 544 // (possibly truncated) to `directory_mtime`
544 545 // 2. This `status` algorithm calls `read_dir`
545 546 // 3. An other change is made to the same directory is
546 547 // made so that calling `read_dir` agin would give
547 548 // different results, but soon enough after 1. that
548 549 // the mtime stays the same
549 550 //
550 551 // On a system where the time resolution poor, this
551 552 // scenario is not unlikely if all three steps are caused
552 553 // by the same script.
553 554 return Ok(());
554 555 }
555 556 } else {
556 557 // OS/libc does not support mtime?
557 558 return Ok(());
558 559 };
559 560 // We’ve observed (through `status_start`) that time has
560 561 // “progressed” since `directory_mtime`, so any further
561 562 // change to this directory is extremely likely to cause a
562 563 // different mtime.
563 564 //
564 565 // Having the same mtime again is not entirely impossible
565 566 // since the system clock is not monotonous. It could jump
566 567 // backward to some point before `directory_mtime`, then a
567 568 // directory change could potentially happen during exactly
568 569 // the wrong tick.
569 570 //
570 571 // We deem this scenario (unlike the previous one) to be
571 572 // unlikely enough in practice.
572 573
573 574 let is_up_to_date =
574 575 if let Some(cached) = dirstate_node.cached_directory_mtime()? {
575 576 cached.likely_equal(directory_mtime)
576 577 } else {
577 578 false
578 579 };
579 580 if !is_up_to_date {
580 581 let hg_path = dirstate_node
581 582 .full_path_borrowed(self.dmap.on_disk)?
582 583 .detach_from_tree();
583 584 self.new_cachable_directories
584 585 .lock()
585 586 .unwrap()
586 587 .push((hg_path, directory_mtime))
587 588 }
588 589 Ok(())
589 590 }
590 591
591 592 /// A file with `EntryState::Normal` in the dirstate was found in the
592 593 /// filesystem
593 594 fn handle_normal_file(
594 595 &self,
595 596 dirstate_node: &NodeRef<'tree, 'on_disk>,
596 597 fs_metadata: &std::fs::Metadata,
597 598 ) -> Result<(), DirstateV2ParseError> {
598 599 // Keep the low 31 bits
599 600 fn truncate_u64(value: u64) -> i32 {
600 601 (value & 0x7FFF_FFFF) as i32
601 602 }
602 603
603 604 let entry = dirstate_node
604 605 .entry()?
605 606 .expect("handle_normal_file called with entry-less node");
606 607 let mode_changed =
607 608 || self.options.check_exec && entry.mode_changed(fs_metadata);
608 609 let size = entry.size();
609 610 let size_changed = size != truncate_u64(fs_metadata.len());
610 611 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
611 612 // issue6456: Size returned may be longer due to encryption
612 613 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
613 614 self.push_outcome(Outcome::Unsure, dirstate_node)?
614 615 } else if dirstate_node.has_copy_source()
615 616 || entry.is_from_other_parent()
616 617 || (size >= 0 && (size_changed || mode_changed()))
617 618 {
618 619 self.push_outcome(Outcome::Modified, dirstate_node)?
619 620 } else {
620 621 let mtime_looks_clean;
621 622 if let Some(dirstate_mtime) = entry.truncated_mtime() {
622 623 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
623 624 .expect("OS/libc does not support mtime?");
624 625 // There might be a change in the future if for example the
625 626 // internal clock become off while process run, but this is a
626 627 // case where the issues the user would face
627 628 // would be a lot worse and there is nothing we
628 629 // can really do.
629 630 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
630 631 } else {
631 632 // No mtime in the dirstate entry
632 633 mtime_looks_clean = false
633 634 };
634 635 if !mtime_looks_clean {
635 636 self.push_outcome(Outcome::Unsure, dirstate_node)?
636 637 } else if self.options.list_clean {
637 638 self.push_outcome(Outcome::Clean, dirstate_node)?
638 639 }
639 640 }
640 641 Ok(())
641 642 }
642 643
643 644 /// A node in the dirstate tree has no corresponding filesystem entry
644 645 fn traverse_dirstate_only(
645 646 &self,
646 647 dirstate_node: NodeRef<'tree, 'on_disk>,
647 648 ) -> Result<(), DirstateV2ParseError> {
648 649 self.check_for_outdated_directory_cache(&dirstate_node)?;
649 650 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
650 651 dirstate_node
651 652 .children(self.dmap.on_disk)?
652 653 .par_iter()
653 654 .map(|child_node| self.traverse_dirstate_only(child_node))
654 655 .collect()
655 656 }
656 657
657 658 /// A node in the dirstate tree has no corresponding *file* on the
658 659 /// filesystem
659 660 ///
660 661 /// Does nothing on a "directory" node
661 662 fn mark_removed_or_deleted_if_file(
662 663 &self,
663 664 dirstate_node: &NodeRef<'tree, 'on_disk>,
664 665 ) -> Result<(), DirstateV2ParseError> {
665 666 if let Some(state) = dirstate_node.state()? {
666 667 let path = dirstate_node.full_path(self.dmap.on_disk)?;
667 668 if self.matcher.matches(path) {
668 669 if let EntryState::Removed = state {
669 670 self.push_outcome(Outcome::Removed, dirstate_node)?
670 671 } else {
671 672 self.push_outcome(Outcome::Deleted, &dirstate_node)?
672 673 }
673 674 }
674 675 }
675 676 Ok(())
676 677 }
677 678
678 679 /// Something in the filesystem has no corresponding dirstate node
679 680 ///
680 681 /// Returns whether that path is ignored
681 682 fn traverse_fs_only(
682 683 &self,
683 684 has_ignored_ancestor: bool,
684 685 directory_hg_path: &HgPath,
685 686 fs_entry: &DirEntry,
686 687 ) -> bool {
687 688 let hg_path = directory_hg_path.join(&fs_entry.base_name);
688 689 let file_type = fs_entry.metadata.file_type();
689 690 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
690 691 if file_type.is_dir() {
691 692 let is_ignored =
692 693 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
693 694 let traverse_children = if is_ignored {
694 695 // Descendants of an ignored directory are all ignored
695 696 self.options.list_ignored
696 697 } else {
697 698 // Descendants of an unknown directory may be either unknown or
698 699 // ignored
699 700 self.options.list_unknown || self.options.list_ignored
700 701 };
701 702 if traverse_children {
702 703 let is_at_repo_root = false;
703 704 if let Ok(children_fs_entries) = self.read_dir(
704 705 &hg_path,
705 706 &fs_entry.full_path,
706 707 is_at_repo_root,
707 708 ) {
708 709 children_fs_entries.par_iter().for_each(|child_fs_entry| {
709 710 self.traverse_fs_only(
710 711 is_ignored,
711 712 &hg_path,
712 713 child_fs_entry,
713 714 );
714 715 })
715 716 }
716 717 }
717 718 if self.options.collect_traversed_dirs {
718 719 self.outcome.lock().unwrap().traversed.push(hg_path.into())
719 720 }
720 721 is_ignored
721 722 } else {
722 723 if file_or_symlink {
723 724 if self.matcher.matches(&hg_path) {
724 725 self.mark_unknown_or_ignored(
725 726 has_ignored_ancestor,
726 727 &BorrowedPath::InMemory(&hg_path),
727 728 )
728 729 } else {
729 730 // We haven’t computed whether this path is ignored. It
730 731 // might not be, and a future run of status might have a
731 732 // different matcher that matches it. So treat it as not
732 733 // ignored. That is, inhibit readdir caching of the parent
733 734 // directory.
734 735 false
735 736 }
736 737 } else {
737 738 // This is neither a directory, a plain file, or a symlink.
738 739 // Treat it like an ignored file.
739 740 true
740 741 }
741 742 }
742 743 }
743 744
744 745 /// Returns whether that path is ignored
745 746 fn mark_unknown_or_ignored(
746 747 &self,
747 748 has_ignored_ancestor: bool,
748 749 hg_path: &BorrowedPath<'_, 'on_disk>,
749 750 ) -> bool {
750 751 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
751 752 if is_ignored {
752 753 if self.options.list_ignored {
753 754 self.push_outcome_without_copy_source(
754 755 Outcome::Ignored,
755 756 hg_path,
756 757 )
757 758 }
758 759 } else {
759 760 if self.options.list_unknown {
760 761 self.push_outcome_without_copy_source(
761 762 Outcome::Unknown,
762 763 hg_path,
763 764 )
764 765 }
765 766 }
766 767 is_ignored
767 768 }
768 769 }
769 770
770 771 struct DirEntry {
771 772 base_name: HgPathBuf,
772 773 full_path: PathBuf,
773 774 metadata: std::fs::Metadata,
774 775 }
775 776
776 777 impl DirEntry {
777 778 /// Returns **unsorted** entries in the given directory, with name and
778 779 /// metadata.
779 780 ///
780 781 /// If a `.hg` sub-directory is encountered:
781 782 ///
782 783 /// * At the repository root, ignore that sub-directory
783 784 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
784 785 /// list instead.
785 786 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
786 787 // `read_dir` returns a "not found" error for the empty path
787 788 let at_cwd = path == Path::new("");
788 789 let read_dir_path = if at_cwd { Path::new(".") } else { path };
789 790 let mut results = Vec::new();
790 791 for entry in read_dir_path.read_dir()? {
791 792 let entry = entry?;
792 793 let metadata = match entry.metadata() {
793 794 Ok(v) => v,
794 795 Err(e) => {
795 796 // race with file deletion?
796 797 if e.kind() == std::io::ErrorKind::NotFound {
797 798 continue;
798 799 } else {
799 800 return Err(e);
800 801 }
801 802 }
802 803 };
803 804 let file_name = entry.file_name();
804 805 // FIXME don't do this when cached
805 806 if file_name == ".hg" {
806 807 if is_at_repo_root {
807 808 // Skip the repo’s own .hg (might be a symlink)
808 809 continue;
809 810 } else if metadata.is_dir() {
810 811 // A .hg sub-directory at another location means a subrepo,
811 812 // skip it entirely.
812 813 return Ok(Vec::new());
813 814 }
814 815 }
815 816 let full_path = if at_cwd {
816 817 file_name.clone().into()
817 818 } else {
818 819 entry.path()
819 820 };
820 821 let base_name = get_bytes_from_os_string(file_name).into();
821 822 results.push(DirEntry {
822 823 base_name,
823 824 full_path,
824 825 metadata,
825 826 })
826 827 }
827 828 Ok(results)
828 829 }
829 830 }
830 831
831 832 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
832 833 /// of the give repository.
833 834 ///
834 835 /// This is similar to `SystemTime::now()`, with the result truncated to the
835 836 /// same time resolution as other files’ modification times. Using `.hg`
836 837 /// instead of the system’s default temporary directory (such as `/tmp`) makes
837 838 /// it more likely the temporary file is in the same disk partition as contents
838 839 /// of the working directory, which can matter since different filesystems may
839 840 /// store timestamps with different resolutions.
840 841 ///
841 842 /// This may fail, typically if we lack write permissions. In that case we
842 843 /// should continue the `status()` algoritm anyway and consider the current
843 844 /// date/time to be unknown.
844 845 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
845 846 tempfile::tempfile_in(repo_root.join(".hg"))?
846 847 .metadata()?
847 848 .modified()
848 849 }
@@ -1,516 +1,509 b''
1 1 use crate::changelog::Changelog;
2 2 use crate::config::{Config, ConfigError, ConfigParseError};
3 3 use crate::dirstate::DirstateParents;
4 use crate::dirstate_tree::dirstate_map::DirstateMap;
5 4 use crate::dirstate_tree::on_disk::Docket as DirstateDocket;
6 5 use crate::dirstate_tree::owning::OwningDirstateMap;
7 6 use crate::errors::HgResultExt;
8 7 use crate::errors::{HgError, IoResultExt};
9 8 use crate::lock::{try_with_lock_no_wait, LockError};
10 9 use crate::manifest::{Manifest, Manifestlog};
11 10 use crate::revlog::filelog::Filelog;
12 11 use crate::revlog::revlog::RevlogError;
13 12 use crate::utils::files::get_path_from_bytes;
14 13 use crate::utils::hg_path::HgPath;
15 14 use crate::utils::SliceExt;
16 15 use crate::vfs::{is_dir, is_file, Vfs};
17 16 use crate::{requirements, NodePrefix};
18 17 use crate::{DirstateError, Revision};
19 18 use std::cell::{Ref, RefCell, RefMut};
20 19 use std::collections::HashSet;
21 20 use std::io::Seek;
22 21 use std::io::SeekFrom;
23 22 use std::io::Write as IoWrite;
24 23 use std::path::{Path, PathBuf};
25 24
26 25 /// A repository on disk
27 26 pub struct Repo {
28 27 working_directory: PathBuf,
29 28 dot_hg: PathBuf,
30 29 store: PathBuf,
31 30 requirements: HashSet<String>,
32 31 config: Config,
33 32 dirstate_parents: LazyCell<DirstateParents, HgError>,
34 33 dirstate_data_file_uuid: LazyCell<Option<Vec<u8>>, HgError>,
35 34 dirstate_map: LazyCell<OwningDirstateMap, DirstateError>,
36 35 changelog: LazyCell<Changelog, HgError>,
37 36 manifestlog: LazyCell<Manifestlog, HgError>,
38 37 }
39 38
40 39 #[derive(Debug, derive_more::From)]
41 40 pub enum RepoError {
42 41 NotFound {
43 42 at: PathBuf,
44 43 },
45 44 #[from]
46 45 ConfigParseError(ConfigParseError),
47 46 #[from]
48 47 Other(HgError),
49 48 }
50 49
51 50 impl From<ConfigError> for RepoError {
52 51 fn from(error: ConfigError) -> Self {
53 52 match error {
54 53 ConfigError::Parse(error) => error.into(),
55 54 ConfigError::Other(error) => error.into(),
56 55 }
57 56 }
58 57 }
59 58
60 59 impl Repo {
61 60 /// tries to find nearest repository root in current working directory or
62 61 /// its ancestors
63 62 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
64 63 let current_directory = crate::utils::current_dir()?;
65 64 // ancestors() is inclusive: it first yields `current_directory`
66 65 // as-is.
67 66 for ancestor in current_directory.ancestors() {
68 67 if is_dir(ancestor.join(".hg"))? {
69 68 return Ok(ancestor.to_path_buf());
70 69 }
71 70 }
72 71 return Err(RepoError::NotFound {
73 72 at: current_directory,
74 73 });
75 74 }
76 75
77 76 /// Find a repository, either at the given path (which must contain a `.hg`
78 77 /// sub-directory) or by searching the current directory and its
79 78 /// ancestors.
80 79 ///
81 80 /// A method with two very different "modes" like this usually a code smell
82 81 /// to make two methods instead, but in this case an `Option` is what rhg
83 82 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
84 83 /// Having two methods would just move that `if` to almost all callers.
85 84 pub fn find(
86 85 config: &Config,
87 86 explicit_path: Option<PathBuf>,
88 87 ) -> Result<Self, RepoError> {
89 88 if let Some(root) = explicit_path {
90 89 if is_dir(root.join(".hg"))? {
91 90 Self::new_at_path(root.to_owned(), config)
92 91 } else if is_file(&root)? {
93 92 Err(HgError::unsupported("bundle repository").into())
94 93 } else {
95 94 Err(RepoError::NotFound {
96 95 at: root.to_owned(),
97 96 })
98 97 }
99 98 } else {
100 99 let root = Self::find_repo_root()?;
101 100 Self::new_at_path(root, config)
102 101 }
103 102 }
104 103
105 104 /// To be called after checking that `.hg` is a sub-directory
106 105 fn new_at_path(
107 106 working_directory: PathBuf,
108 107 config: &Config,
109 108 ) -> Result<Self, RepoError> {
110 109 let dot_hg = working_directory.join(".hg");
111 110
112 111 let mut repo_config_files = Vec::new();
113 112 repo_config_files.push(dot_hg.join("hgrc"));
114 113 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
115 114
116 115 let hg_vfs = Vfs { base: &dot_hg };
117 116 let mut reqs = requirements::load_if_exists(hg_vfs)?;
118 117 let relative =
119 118 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
120 119 let shared =
121 120 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
122 121
123 122 // From `mercurial/localrepo.py`:
124 123 //
125 124 // if .hg/requires contains the sharesafe requirement, it means
126 125 // there exists a `.hg/store/requires` too and we should read it
127 126 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
128 127 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
129 128 // is not present, refer checkrequirementscompat() for that
130 129 //
131 130 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
132 131 // repository was shared the old way. We check the share source
133 132 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
134 133 // current repository needs to be reshared
135 134 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
136 135
137 136 let store_path;
138 137 if !shared {
139 138 store_path = dot_hg.join("store");
140 139 } else {
141 140 let bytes = hg_vfs.read("sharedpath")?;
142 141 let mut shared_path =
143 142 get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n'))
144 143 .to_owned();
145 144 if relative {
146 145 shared_path = dot_hg.join(shared_path)
147 146 }
148 147 if !is_dir(&shared_path)? {
149 148 return Err(HgError::corrupted(format!(
150 149 ".hg/sharedpath points to nonexistent directory {}",
151 150 shared_path.display()
152 151 ))
153 152 .into());
154 153 }
155 154
156 155 store_path = shared_path.join("store");
157 156
158 157 let source_is_share_safe =
159 158 requirements::load(Vfs { base: &shared_path })?
160 159 .contains(requirements::SHARESAFE_REQUIREMENT);
161 160
162 161 if share_safe != source_is_share_safe {
163 162 return Err(HgError::unsupported("share-safe mismatch").into());
164 163 }
165 164
166 165 if share_safe {
167 166 repo_config_files.insert(0, shared_path.join("hgrc"))
168 167 }
169 168 }
170 169 if share_safe {
171 170 reqs.extend(requirements::load(Vfs { base: &store_path })?);
172 171 }
173 172
174 173 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
175 174 config.combine_with_repo(&repo_config_files)?
176 175 } else {
177 176 config.clone()
178 177 };
179 178
180 179 let repo = Self {
181 180 requirements: reqs,
182 181 working_directory,
183 182 store: store_path,
184 183 dot_hg,
185 184 config: repo_config,
186 185 dirstate_parents: LazyCell::new(Self::read_dirstate_parents),
187 186 dirstate_data_file_uuid: LazyCell::new(
188 187 Self::read_dirstate_data_file_uuid,
189 188 ),
190 189 dirstate_map: LazyCell::new(Self::new_dirstate_map),
191 190 changelog: LazyCell::new(Changelog::open),
192 191 manifestlog: LazyCell::new(Manifestlog::open),
193 192 };
194 193
195 194 requirements::check(&repo)?;
196 195
197 196 Ok(repo)
198 197 }
199 198
200 199 pub fn working_directory_path(&self) -> &Path {
201 200 &self.working_directory
202 201 }
203 202
204 203 pub fn requirements(&self) -> &HashSet<String> {
205 204 &self.requirements
206 205 }
207 206
208 207 pub fn config(&self) -> &Config {
209 208 &self.config
210 209 }
211 210
212 211 /// For accessing repository files (in `.hg`), except for the store
213 212 /// (`.hg/store`).
214 213 pub fn hg_vfs(&self) -> Vfs<'_> {
215 214 Vfs { base: &self.dot_hg }
216 215 }
217 216
218 217 /// For accessing repository store files (in `.hg/store`)
219 218 pub fn store_vfs(&self) -> Vfs<'_> {
220 219 Vfs { base: &self.store }
221 220 }
222 221
223 222 /// For accessing the working copy
224 223 pub fn working_directory_vfs(&self) -> Vfs<'_> {
225 224 Vfs {
226 225 base: &self.working_directory,
227 226 }
228 227 }
229 228
230 229 pub fn try_with_wlock_no_wait<R>(
231 230 &self,
232 231 f: impl FnOnce() -> R,
233 232 ) -> Result<R, LockError> {
234 233 try_with_lock_no_wait(self.hg_vfs(), "wlock", f)
235 234 }
236 235
237 236 pub fn has_dirstate_v2(&self) -> bool {
238 237 self.requirements
239 238 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
240 239 }
241 240
242 241 pub fn has_sparse(&self) -> bool {
243 242 self.requirements.contains(requirements::SPARSE_REQUIREMENT)
244 243 }
245 244
246 245 pub fn has_narrow(&self) -> bool {
247 246 self.requirements.contains(requirements::NARROW_REQUIREMENT)
248 247 }
249 248
250 249 fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
251 250 Ok(self
252 251 .hg_vfs()
253 252 .read("dirstate")
254 253 .io_not_found_as_none()?
255 254 .unwrap_or(Vec::new()))
256 255 }
257 256
258 257 pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> {
259 258 Ok(*self.dirstate_parents.get_or_init(self)?)
260 259 }
261 260
262 261 fn read_dirstate_parents(&self) -> Result<DirstateParents, HgError> {
263 262 let dirstate = self.dirstate_file_contents()?;
264 263 let parents = if dirstate.is_empty() {
265 264 if self.has_dirstate_v2() {
266 265 self.dirstate_data_file_uuid.set(None);
267 266 }
268 267 DirstateParents::NULL
269 268 } else if self.has_dirstate_v2() {
270 269 let docket =
271 270 crate::dirstate_tree::on_disk::read_docket(&dirstate)?;
272 271 self.dirstate_data_file_uuid
273 272 .set(Some(docket.uuid.to_owned()));
274 273 docket.parents()
275 274 } else {
276 275 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
277 276 .clone()
278 277 };
279 278 self.dirstate_parents.set(parents);
280 279 Ok(parents)
281 280 }
282 281
283 282 fn read_dirstate_data_file_uuid(
284 283 &self,
285 284 ) -> Result<Option<Vec<u8>>, HgError> {
286 285 assert!(
287 286 self.has_dirstate_v2(),
288 287 "accessing dirstate data file ID without dirstate-v2"
289 288 );
290 289 let dirstate = self.dirstate_file_contents()?;
291 290 if dirstate.is_empty() {
292 291 self.dirstate_parents.set(DirstateParents::NULL);
293 292 Ok(None)
294 293 } else {
295 294 let docket =
296 295 crate::dirstate_tree::on_disk::read_docket(&dirstate)?;
297 296 self.dirstate_parents.set(docket.parents());
298 297 Ok(Some(docket.uuid.to_owned()))
299 298 }
300 299 }
301 300
302 301 fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> {
303 302 let dirstate_file_contents = self.dirstate_file_contents()?;
304 303 if dirstate_file_contents.is_empty() {
305 304 self.dirstate_parents.set(DirstateParents::NULL);
306 305 if self.has_dirstate_v2() {
307 306 self.dirstate_data_file_uuid.set(None);
308 307 }
309 308 Ok(OwningDirstateMap::new_empty(Vec::new()))
310 309 } else if self.has_dirstate_v2() {
311 310 let docket = crate::dirstate_tree::on_disk::read_docket(
312 311 &dirstate_file_contents,
313 312 )?;
314 313 self.dirstate_parents.set(docket.parents());
315 314 self.dirstate_data_file_uuid
316 315 .set(Some(docket.uuid.to_owned()));
317 316 let data_size = docket.data_size();
318 317 let metadata = docket.tree_metadata();
319 let mut map = if let Some(data_mmap) = self
318 if let Some(data_mmap) = self
320 319 .hg_vfs()
321 320 .mmap_open(docket.data_filename())
322 321 .io_not_found_as_none()?
323 322 {
324 OwningDirstateMap::new_empty(data_mmap)
323 OwningDirstateMap::new_v2(data_mmap, data_size, metadata)
325 324 } else {
326 OwningDirstateMap::new_empty(Vec::new())
327 };
328 let (on_disk, placeholder) = map.get_pair_mut();
329 *placeholder = DirstateMap::new_v2(on_disk, data_size, metadata)?;
330 Ok(map)
325 OwningDirstateMap::new_v2(Vec::new(), data_size, metadata)
326 }
331 327 } else {
332 let mut map = OwningDirstateMap::new_empty(dirstate_file_contents);
333 let (on_disk, placeholder) = map.get_pair_mut();
334 let (inner, parents) = DirstateMap::new_v1(on_disk)?;
335 self.dirstate_parents
336 .set(parents.unwrap_or(DirstateParents::NULL));
337 *placeholder = inner;
328 let (map, parents) =
329 OwningDirstateMap::new_v1(dirstate_file_contents)?;
330 self.dirstate_parents.set(parents);
338 331 Ok(map)
339 332 }
340 333 }
341 334
342 335 pub fn dirstate_map(
343 336 &self,
344 337 ) -> Result<Ref<OwningDirstateMap>, DirstateError> {
345 338 self.dirstate_map.get_or_init(self)
346 339 }
347 340
348 341 pub fn dirstate_map_mut(
349 342 &self,
350 343 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
351 344 self.dirstate_map.get_mut_or_init(self)
352 345 }
353 346
354 347 pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> {
355 348 self.changelog.get_or_init(self)
356 349 }
357 350
358 351 pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> {
359 352 self.changelog.get_mut_or_init(self)
360 353 }
361 354
362 355 pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> {
363 356 self.manifestlog.get_or_init(self)
364 357 }
365 358
366 359 pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> {
367 360 self.manifestlog.get_mut_or_init(self)
368 361 }
369 362
370 363 /// Returns the manifest of the *changeset* with the given node ID
371 364 pub fn manifest_for_node(
372 365 &self,
373 366 node: impl Into<NodePrefix>,
374 367 ) -> Result<Manifest, RevlogError> {
375 368 self.manifestlog()?.data_for_node(
376 369 self.changelog()?
377 370 .data_for_node(node.into())?
378 371 .manifest_node()?
379 372 .into(),
380 373 )
381 374 }
382 375
383 376 /// Returns the manifest of the *changeset* with the given revision number
384 377 pub fn manifest_for_rev(
385 378 &self,
386 379 revision: Revision,
387 380 ) -> Result<Manifest, RevlogError> {
388 381 self.manifestlog()?.data_for_node(
389 382 self.changelog()?
390 383 .data_for_rev(revision)?
391 384 .manifest_node()?
392 385 .into(),
393 386 )
394 387 }
395 388
396 389 pub fn has_subrepos(&self) -> Result<bool, DirstateError> {
397 390 if let Some(entry) = self.dirstate_map()?.get(HgPath::new(".hgsub"))? {
398 391 Ok(entry.state().is_tracked())
399 392 } else {
400 393 Ok(false)
401 394 }
402 395 }
403 396
404 397 pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> {
405 398 Filelog::open(self, path)
406 399 }
407 400
408 401 /// Write to disk any updates that were made through `dirstate_map_mut`.
409 402 ///
410 403 /// The "wlock" must be held while calling this.
411 404 /// See for example `try_with_wlock_no_wait`.
412 405 ///
413 406 /// TODO: have a `WritableRepo` type only accessible while holding the
414 407 /// lock?
415 408 pub fn write_dirstate(&self) -> Result<(), DirstateError> {
416 409 let map = self.dirstate_map()?;
417 410 // TODO: Maintain a `DirstateMap::dirty` flag, and return early here if
418 411 // it’s unset
419 412 let parents = self.dirstate_parents()?;
420 413 let packed_dirstate = if self.has_dirstate_v2() {
421 414 let uuid = self.dirstate_data_file_uuid.get_or_init(self)?;
422 415 let mut uuid = uuid.as_ref();
423 416 let can_append = uuid.is_some();
424 417 let (data, tree_metadata, append) = map.pack_v2(can_append)?;
425 418 if !append {
426 419 uuid = None
427 420 }
428 421 let uuid = if let Some(uuid) = uuid {
429 422 std::str::from_utf8(uuid)
430 423 .map_err(|_| {
431 424 HgError::corrupted("non-UTF-8 dirstate data file ID")
432 425 })?
433 426 .to_owned()
434 427 } else {
435 428 DirstateDocket::new_uid()
436 429 };
437 430 let data_filename = format!("dirstate.{}", uuid);
438 431 let data_filename = self.hg_vfs().join(data_filename);
439 432 let mut options = std::fs::OpenOptions::new();
440 433 if append {
441 434 options.append(true);
442 435 } else {
443 436 options.write(true).create_new(true);
444 437 }
445 438 let data_size = (|| {
446 439 // TODO: loop and try another random ID if !append and this
447 440 // returns `ErrorKind::AlreadyExists`? Collision chance of two
448 441 // random IDs is one in 2**32
449 442 let mut file = options.open(&data_filename)?;
450 443 file.write_all(&data)?;
451 444 file.flush()?;
452 445 // TODO: use https://doc.rust-lang.org/std/io/trait.Seek.html#method.stream_position when we require Rust 1.51+
453 446 file.seek(SeekFrom::Current(0))
454 447 })()
455 448 .when_writing_file(&data_filename)?;
456 449 DirstateDocket::serialize(
457 450 parents,
458 451 tree_metadata,
459 452 data_size,
460 453 uuid.as_bytes(),
461 454 )
462 455 .map_err(|_: std::num::TryFromIntError| {
463 456 HgError::corrupted("overflow in dirstate docket serialization")
464 457 })?
465 458 } else {
466 459 map.pack_v1(parents)?
467 460 };
468 461 self.hg_vfs().atomic_write("dirstate", &packed_dirstate)?;
469 462 Ok(())
470 463 }
471 464 }
472 465
473 466 /// Lazily-initialized component of `Repo` with interior mutability
474 467 ///
475 468 /// This differs from `OnceCell` in that the value can still be "deinitialized"
476 469 /// later by setting its inner `Option` to `None`.
477 470 struct LazyCell<T, E> {
478 471 value: RefCell<Option<T>>,
479 472 // `Fn`s that don’t capture environment are zero-size, so this box does
480 473 // not allocate:
481 474 init: Box<dyn Fn(&Repo) -> Result<T, E>>,
482 475 }
483 476
484 477 impl<T, E> LazyCell<T, E> {
485 478 fn new(init: impl Fn(&Repo) -> Result<T, E> + 'static) -> Self {
486 479 Self {
487 480 value: RefCell::new(None),
488 481 init: Box::new(init),
489 482 }
490 483 }
491 484
492 485 fn set(&self, value: T) {
493 486 *self.value.borrow_mut() = Some(value)
494 487 }
495 488
496 489 fn get_or_init(&self, repo: &Repo) -> Result<Ref<T>, E> {
497 490 let mut borrowed = self.value.borrow();
498 491 if borrowed.is_none() {
499 492 drop(borrowed);
500 493 // Only use `borrow_mut` if it is really needed to avoid panic in
501 494 // case there is another outstanding borrow but mutation is not
502 495 // needed.
503 496 *self.value.borrow_mut() = Some((self.init)(repo)?);
504 497 borrowed = self.value.borrow()
505 498 }
506 499 Ok(Ref::map(borrowed, |option| option.as_ref().unwrap()))
507 500 }
508 501
509 502 fn get_mut_or_init(&self, repo: &Repo) -> Result<RefMut<T>, E> {
510 503 let mut borrowed = self.value.borrow_mut();
511 504 if borrowed.is_none() {
512 505 *borrowed = Some((self.init)(repo)?);
513 506 }
514 507 Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap()))
515 508 }
516 509 }
@@ -1,820 +1,813 b''
1 1 // hg_path.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use crate::utils::SliceExt;
9 9 use std::borrow::Borrow;
10 10 use std::borrow::Cow;
11 11 use std::convert::TryFrom;
12 12 use std::ffi::{OsStr, OsString};
13 13 use std::fmt;
14 14 use std::ops::Deref;
15 15 use std::path::{Path, PathBuf};
16 16
17 17 #[derive(Debug, Eq, PartialEq)]
18 18 pub enum HgPathError {
19 19 /// Bytes from the invalid `HgPath`
20 20 LeadingSlash(Vec<u8>),
21 21 ConsecutiveSlashes {
22 22 bytes: Vec<u8>,
23 23 second_slash_index: usize,
24 24 },
25 25 ContainsNullByte {
26 26 bytes: Vec<u8>,
27 27 null_byte_index: usize,
28 28 },
29 29 /// Bytes
30 30 DecodeError(Vec<u8>),
31 31 /// The rest come from audit errors
32 32 EndsWithSlash(HgPathBuf),
33 33 ContainsIllegalComponent(HgPathBuf),
34 34 /// Path is inside the `.hg` folder
35 35 InsideDotHg(HgPathBuf),
36 36 IsInsideNestedRepo {
37 37 path: HgPathBuf,
38 38 nested_repo: HgPathBuf,
39 39 },
40 40 TraversesSymbolicLink {
41 41 path: HgPathBuf,
42 42 symlink: HgPathBuf,
43 43 },
44 44 NotFsCompliant(HgPathBuf),
45 45 /// `path` is the smallest invalid path
46 46 NotUnderRoot {
47 47 path: PathBuf,
48 48 root: PathBuf,
49 49 },
50 50 }
51 51
52 52 impl fmt::Display for HgPathError {
53 53 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54 54 match self {
55 55 HgPathError::LeadingSlash(bytes) => {
56 56 write!(f, "Invalid HgPath '{:?}': has a leading slash.", bytes)
57 57 }
58 58 HgPathError::ConsecutiveSlashes {
59 59 bytes,
60 60 second_slash_index: pos,
61 61 } => write!(
62 62 f,
63 63 "Invalid HgPath '{:?}': consecutive slashes at pos {}.",
64 64 bytes, pos
65 65 ),
66 66 HgPathError::ContainsNullByte {
67 67 bytes,
68 68 null_byte_index: pos,
69 69 } => write!(
70 70 f,
71 71 "Invalid HgPath '{:?}': contains null byte at pos {}.",
72 72 bytes, pos
73 73 ),
74 74 HgPathError::DecodeError(bytes) => write!(
75 75 f,
76 76 "Invalid HgPath '{:?}': could not be decoded.",
77 77 bytes
78 78 ),
79 79 HgPathError::EndsWithSlash(path) => {
80 80 write!(f, "Audit failed for '{}': ends with a slash.", path)
81 81 }
82 82 HgPathError::ContainsIllegalComponent(path) => write!(
83 83 f,
84 84 "Audit failed for '{}': contains an illegal component.",
85 85 path
86 86 ),
87 87 HgPathError::InsideDotHg(path) => write!(
88 88 f,
89 89 "Audit failed for '{}': is inside the '.hg' folder.",
90 90 path
91 91 ),
92 92 HgPathError::IsInsideNestedRepo {
93 93 path,
94 94 nested_repo: nested,
95 95 } => {
96 96 write!(f,
97 97 "Audit failed for '{}': is inside a nested repository '{}'.",
98 98 path, nested
99 99 )
100 100 }
101 101 HgPathError::TraversesSymbolicLink { path, symlink } => write!(
102 102 f,
103 103 "Audit failed for '{}': traverses symbolic link '{}'.",
104 104 path, symlink
105 105 ),
106 106 HgPathError::NotFsCompliant(path) => write!(
107 107 f,
108 108 "Audit failed for '{}': cannot be turned into a \
109 109 filesystem path.",
110 110 path
111 111 ),
112 112 HgPathError::NotUnderRoot { path, root } => write!(
113 113 f,
114 114 "Audit failed for '{}': not under root {}.",
115 115 path.display(),
116 116 root.display()
117 117 ),
118 118 }
119 119 }
120 120 }
121 121
122 122 impl From<HgPathError> for std::io::Error {
123 123 fn from(e: HgPathError) -> Self {
124 124 std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
125 125 }
126 126 }
127 127
128 128 /// This is a repository-relative path (or canonical path):
129 129 /// - no null characters
130 130 /// - `/` separates directories
131 131 /// - no consecutive slashes
132 132 /// - no leading slash,
133 133 /// - no `.` nor `..` of special meaning
134 134 /// - stored in repository and shared across platforms
135 135 ///
136 136 /// Note: there is no guarantee of any `HgPath` being well-formed at any point
137 137 /// in its lifetime for performance reasons and to ease ergonomics. It is
138 138 /// however checked using the `check_state` method before any file-system
139 139 /// operation.
140 140 ///
141 141 /// This allows us to be encoding-transparent as much as possible, until really
142 142 /// needed; `HgPath` can be transformed into a platform-specific path (`OsStr`
143 143 /// or `Path`) whenever more complex operations are needed:
144 144 /// On Unix, it's just byte-to-byte conversion. On Windows, it has to be
145 145 /// decoded from MBCS to WTF-8. If WindowsUTF8Plan is implemented, the source
146 146 /// character encoding will be determined on a per-repository basis.
147 //
148 // FIXME: (adapted from a comment in the stdlib)
149 // `HgPath::new()` current implementation relies on `Slice` being
150 // layout-compatible with `[u8]`.
151 // When attribute privacy is implemented, `Slice` should be annotated as
152 // `#[repr(transparent)]`.
153 // Anyway, `Slice` representation and layout are considered implementation
154 // detail, are not documented and must not be relied upon.
155 147 #[derive(Eq, Ord, PartialEq, PartialOrd, Hash)]
148 #[repr(transparent)]
156 149 pub struct HgPath {
157 150 inner: [u8],
158 151 }
159 152
160 153 impl HgPath {
161 154 pub fn new<S: AsRef<[u8]> + ?Sized>(s: &S) -> &Self {
162 155 unsafe { &*(s.as_ref() as *const [u8] as *const Self) }
163 156 }
164 157 pub fn is_empty(&self) -> bool {
165 158 self.inner.is_empty()
166 159 }
167 160 pub fn len(&self) -> usize {
168 161 self.inner.len()
169 162 }
170 163 fn to_hg_path_buf(&self) -> HgPathBuf {
171 164 HgPathBuf {
172 165 inner: self.inner.to_owned(),
173 166 }
174 167 }
175 168 pub fn bytes(&self) -> std::slice::Iter<u8> {
176 169 self.inner.iter()
177 170 }
178 171 pub fn to_ascii_uppercase(&self) -> HgPathBuf {
179 172 HgPathBuf::from(self.inner.to_ascii_uppercase())
180 173 }
181 174 pub fn to_ascii_lowercase(&self) -> HgPathBuf {
182 175 HgPathBuf::from(self.inner.to_ascii_lowercase())
183 176 }
184 177 pub fn as_bytes(&self) -> &[u8] {
185 178 &self.inner
186 179 }
187 180 pub fn contains(&self, other: u8) -> bool {
188 181 self.inner.contains(&other)
189 182 }
190 183 pub fn starts_with(&self, needle: impl AsRef<Self>) -> bool {
191 184 self.inner.starts_with(needle.as_ref().as_bytes())
192 185 }
193 186 pub fn trim_trailing_slash(&self) -> &Self {
194 187 Self::new(if self.inner.last() == Some(&b'/') {
195 188 &self.inner[..self.inner.len() - 1]
196 189 } else {
197 190 &self.inner[..]
198 191 })
199 192 }
200 193 /// Returns a tuple of slices `(base, filename)` resulting from the split
201 194 /// at the rightmost `/`, if any.
202 195 ///
203 196 /// # Examples:
204 197 ///
205 198 /// ```
206 199 /// use hg::utils::hg_path::HgPath;
207 200 ///
208 201 /// let path = HgPath::new(b"cool/hg/path").split_filename();
209 202 /// assert_eq!(path, (HgPath::new(b"cool/hg"), HgPath::new(b"path")));
210 203 ///
211 204 /// let path = HgPath::new(b"pathwithoutsep").split_filename();
212 205 /// assert_eq!(path, (HgPath::new(b""), HgPath::new(b"pathwithoutsep")));
213 206 /// ```
214 207 pub fn split_filename(&self) -> (&Self, &Self) {
215 208 match &self.inner.iter().rposition(|c| *c == b'/') {
216 209 None => (HgPath::new(""), &self),
217 210 Some(size) => (
218 211 HgPath::new(&self.inner[..*size]),
219 212 HgPath::new(&self.inner[*size + 1..]),
220 213 ),
221 214 }
222 215 }
223 216
224 217 pub fn join(&self, path: &HgPath) -> HgPathBuf {
225 218 let mut buf = self.to_owned();
226 219 buf.push(path);
227 220 buf
228 221 }
229 222
230 223 pub fn components(&self) -> impl Iterator<Item = &HgPath> {
231 224 self.inner.split(|&byte| byte == b'/').map(HgPath::new)
232 225 }
233 226
234 227 /// Returns the first (that is "root-most") slash-separated component of
235 228 /// the path, and the rest after the first slash if there is one.
236 229 pub fn split_first_component(&self) -> (&HgPath, Option<&HgPath>) {
237 230 match self.inner.split_2(b'/') {
238 231 Some((a, b)) => (HgPath::new(a), Some(HgPath::new(b))),
239 232 None => (self, None),
240 233 }
241 234 }
242 235
243 236 pub fn parent(&self) -> &Self {
244 237 let inner = self.as_bytes();
245 238 HgPath::new(match inner.iter().rposition(|b| *b == b'/') {
246 239 Some(pos) => &inner[..pos],
247 240 None => &[],
248 241 })
249 242 }
250 243 /// Given a base directory, returns the slice of `self` relative to the
251 244 /// base directory. If `base` is not a directory (does not end with a
252 245 /// `b'/'`), returns `None`.
253 246 pub fn relative_to(&self, base: impl AsRef<Self>) -> Option<&Self> {
254 247 let base = base.as_ref();
255 248 if base.is_empty() {
256 249 return Some(self);
257 250 }
258 251 let is_dir = base.as_bytes().ends_with(b"/");
259 252 if is_dir && self.starts_with(base) {
260 253 Some(Self::new(&self.inner[base.len()..]))
261 254 } else {
262 255 None
263 256 }
264 257 }
265 258
266 259 #[cfg(windows)]
267 260 /// Copied from the Python stdlib's `os.path.splitdrive` implementation.
268 261 ///
269 262 /// Split a pathname into drive/UNC sharepoint and relative path
270 263 /// specifiers. Returns a 2-tuple (drive_or_unc, path); either part may
271 264 /// be empty.
272 265 ///
273 266 /// If you assign
274 267 /// result = split_drive(p)
275 268 /// It is always true that:
276 269 /// result[0] + result[1] == p
277 270 ///
278 271 /// If the path contained a drive letter, drive_or_unc will contain
279 272 /// everything up to and including the colon.
280 273 /// e.g. split_drive("c:/dir") returns ("c:", "/dir")
281 274 ///
282 275 /// If the path contained a UNC path, the drive_or_unc will contain the
283 276 /// host name and share up to but not including the fourth directory
284 277 /// separator character.
285 278 /// e.g. split_drive("//host/computer/dir") returns ("//host/computer",
286 279 /// "/dir")
287 280 ///
288 281 /// Paths cannot contain both a drive letter and a UNC path.
289 282 pub fn split_drive<'a>(&self) -> (&HgPath, &HgPath) {
290 283 let bytes = self.as_bytes();
291 284 let is_sep = |b| std::path::is_separator(b as char);
292 285
293 286 if self.len() < 2 {
294 287 (HgPath::new(b""), &self)
295 288 } else if is_sep(bytes[0])
296 289 && is_sep(bytes[1])
297 290 && (self.len() == 2 || !is_sep(bytes[2]))
298 291 {
299 292 // Is a UNC path:
300 293 // vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
301 294 // \\machine\mountpoint\directory\etc\...
302 295 // directory ^^^^^^^^^^^^^^^
303 296
304 297 let machine_end_index = bytes[2..].iter().position(|b| is_sep(*b));
305 298 let mountpoint_start_index = if let Some(i) = machine_end_index {
306 299 i + 2
307 300 } else {
308 301 return (HgPath::new(b""), &self);
309 302 };
310 303
311 304 match bytes[mountpoint_start_index + 1..]
312 305 .iter()
313 306 .position(|b| is_sep(*b))
314 307 {
315 308 // A UNC path can't have two slashes in a row
316 309 // (after the initial two)
317 310 Some(0) => (HgPath::new(b""), &self),
318 311 Some(i) => {
319 312 let (a, b) =
320 313 bytes.split_at(mountpoint_start_index + 1 + i);
321 314 (HgPath::new(a), HgPath::new(b))
322 315 }
323 316 None => (&self, HgPath::new(b"")),
324 317 }
325 318 } else if bytes[1] == b':' {
326 319 // Drive path c:\directory
327 320 let (a, b) = bytes.split_at(2);
328 321 (HgPath::new(a), HgPath::new(b))
329 322 } else {
330 323 (HgPath::new(b""), &self)
331 324 }
332 325 }
333 326
334 327 #[cfg(unix)]
335 328 /// Split a pathname into drive and path. On Posix, drive is always empty.
336 329 pub fn split_drive(&self) -> (&HgPath, &HgPath) {
337 330 (HgPath::new(b""), &self)
338 331 }
339 332
340 333 /// Checks for errors in the path, short-circuiting at the first one.
341 334 /// This generates fine-grained errors useful for debugging.
342 335 /// To simply check if the path is valid during tests, use `is_valid`.
343 336 pub fn check_state(&self) -> Result<(), HgPathError> {
344 337 if self.is_empty() {
345 338 return Ok(());
346 339 }
347 340 let bytes = self.as_bytes();
348 341 let mut previous_byte = None;
349 342
350 343 if bytes[0] == b'/' {
351 344 return Err(HgPathError::LeadingSlash(bytes.to_vec()));
352 345 }
353 346 for (index, byte) in bytes.iter().enumerate() {
354 347 match byte {
355 348 0 => {
356 349 return Err(HgPathError::ContainsNullByte {
357 350 bytes: bytes.to_vec(),
358 351 null_byte_index: index,
359 352 })
360 353 }
361 354 b'/' => {
362 355 if previous_byte.is_some() && previous_byte == Some(b'/') {
363 356 return Err(HgPathError::ConsecutiveSlashes {
364 357 bytes: bytes.to_vec(),
365 358 second_slash_index: index,
366 359 });
367 360 }
368 361 }
369 362 _ => (),
370 363 };
371 364 previous_byte = Some(*byte);
372 365 }
373 366 Ok(())
374 367 }
375 368
376 369 #[cfg(test)]
377 370 /// Only usable during tests to force developers to handle invalid states
378 371 fn is_valid(&self) -> bool {
379 372 self.check_state().is_ok()
380 373 }
381 374 }
382 375
383 376 impl fmt::Debug for HgPath {
384 377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 378 write!(f, "HgPath({:?})", String::from_utf8_lossy(&self.inner))
386 379 }
387 380 }
388 381
389 382 impl fmt::Display for HgPath {
390 383 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 384 write!(f, "{}", String::from_utf8_lossy(&self.inner))
392 385 }
393 386 }
394 387
395 388 #[derive(
396 389 Default, Eq, Ord, Clone, PartialEq, PartialOrd, Hash, derive_more::From,
397 390 )]
398 391 pub struct HgPathBuf {
399 392 inner: Vec<u8>,
400 393 }
401 394
402 395 impl HgPathBuf {
403 396 pub fn new() -> Self {
404 397 Default::default()
405 398 }
406 399
407 400 pub fn push<T: ?Sized + AsRef<HgPath>>(&mut self, other: &T) -> () {
408 401 if !self.inner.is_empty() && self.inner.last() != Some(&b'/') {
409 402 self.inner.push(b'/');
410 403 }
411 404 self.inner.extend(other.as_ref().bytes())
412 405 }
413 406
414 407 pub fn push_byte(&mut self, byte: u8) {
415 408 self.inner.push(byte);
416 409 }
417 410 pub fn from_bytes(s: &[u8]) -> HgPathBuf {
418 411 HgPath::new(s).to_owned()
419 412 }
420 413 pub fn into_vec(self) -> Vec<u8> {
421 414 self.inner
422 415 }
423 416 }
424 417
425 418 impl fmt::Debug for HgPathBuf {
426 419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 420 write!(f, "HgPathBuf({:?})", String::from_utf8_lossy(&self.inner))
428 421 }
429 422 }
430 423
431 424 impl fmt::Display for HgPathBuf {
432 425 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 426 write!(f, "{}", String::from_utf8_lossy(&self.inner))
434 427 }
435 428 }
436 429
437 430 impl Deref for HgPathBuf {
438 431 type Target = HgPath;
439 432
440 433 #[inline]
441 434 fn deref(&self) -> &HgPath {
442 435 &HgPath::new(&self.inner)
443 436 }
444 437 }
445 438
446 439 impl<T: ?Sized + AsRef<HgPath>> From<&T> for HgPathBuf {
447 440 fn from(s: &T) -> HgPathBuf {
448 441 s.as_ref().to_owned()
449 442 }
450 443 }
451 444
452 445 impl Into<Vec<u8>> for HgPathBuf {
453 446 fn into(self) -> Vec<u8> {
454 447 self.inner
455 448 }
456 449 }
457 450
458 451 impl Borrow<HgPath> for HgPathBuf {
459 452 fn borrow(&self) -> &HgPath {
460 453 &HgPath::new(self.as_bytes())
461 454 }
462 455 }
463 456
464 457 impl ToOwned for HgPath {
465 458 type Owned = HgPathBuf;
466 459
467 460 fn to_owned(&self) -> HgPathBuf {
468 461 self.to_hg_path_buf()
469 462 }
470 463 }
471 464
472 465 impl AsRef<HgPath> for HgPath {
473 466 fn as_ref(&self) -> &HgPath {
474 467 self
475 468 }
476 469 }
477 470
478 471 impl AsRef<HgPath> for HgPathBuf {
479 472 fn as_ref(&self) -> &HgPath {
480 473 self
481 474 }
482 475 }
483 476
484 477 impl Extend<u8> for HgPathBuf {
485 478 fn extend<T: IntoIterator<Item = u8>>(&mut self, iter: T) {
486 479 self.inner.extend(iter);
487 480 }
488 481 }
489 482
490 483 /// TODO: Once https://www.mercurial-scm.org/wiki/WindowsUTF8Plan is
491 484 /// implemented, these conversion utils will have to work differently depending
492 485 /// on the repository encoding: either `UTF-8` or `MBCS`.
493 486
494 487 pub fn hg_path_to_os_string<P: AsRef<HgPath>>(
495 488 hg_path: P,
496 489 ) -> Result<OsString, HgPathError> {
497 490 hg_path.as_ref().check_state()?;
498 491 let os_str;
499 492 #[cfg(unix)]
500 493 {
501 494 use std::os::unix::ffi::OsStrExt;
502 495 os_str = std::ffi::OsStr::from_bytes(&hg_path.as_ref().as_bytes());
503 496 }
504 497 // TODO Handle other platforms
505 498 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
506 499 Ok(os_str.to_os_string())
507 500 }
508 501
509 502 pub fn hg_path_to_path_buf<P: AsRef<HgPath>>(
510 503 hg_path: P,
511 504 ) -> Result<PathBuf, HgPathError> {
512 505 Ok(Path::new(&hg_path_to_os_string(hg_path)?).to_path_buf())
513 506 }
514 507
515 508 pub fn os_string_to_hg_path_buf<S: AsRef<OsStr>>(
516 509 os_string: S,
517 510 ) -> Result<HgPathBuf, HgPathError> {
518 511 let buf;
519 512 #[cfg(unix)]
520 513 {
521 514 use std::os::unix::ffi::OsStrExt;
522 515 buf = HgPathBuf::from_bytes(&os_string.as_ref().as_bytes());
523 516 }
524 517 // TODO Handle other platforms
525 518 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
526 519
527 520 buf.check_state()?;
528 521 Ok(buf)
529 522 }
530 523
531 524 pub fn path_to_hg_path_buf<P: AsRef<Path>>(
532 525 path: P,
533 526 ) -> Result<HgPathBuf, HgPathError> {
534 527 let buf;
535 528 let os_str = path.as_ref().as_os_str();
536 529 #[cfg(unix)]
537 530 {
538 531 use std::os::unix::ffi::OsStrExt;
539 532 buf = HgPathBuf::from_bytes(&os_str.as_bytes());
540 533 }
541 534 // TODO Handle other platforms
542 535 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
543 536
544 537 buf.check_state()?;
545 538 Ok(buf)
546 539 }
547 540
548 541 impl TryFrom<PathBuf> for HgPathBuf {
549 542 type Error = HgPathError;
550 543 fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
551 544 path_to_hg_path_buf(path)
552 545 }
553 546 }
554 547
555 548 impl From<HgPathBuf> for Cow<'_, HgPath> {
556 549 fn from(path: HgPathBuf) -> Self {
557 550 Cow::Owned(path)
558 551 }
559 552 }
560 553
561 554 impl<'a> From<&'a HgPath> for Cow<'a, HgPath> {
562 555 fn from(path: &'a HgPath) -> Self {
563 556 Cow::Borrowed(path)
564 557 }
565 558 }
566 559
567 560 impl<'a> From<&'a HgPathBuf> for Cow<'a, HgPath> {
568 561 fn from(path: &'a HgPathBuf) -> Self {
569 562 Cow::Borrowed(&**path)
570 563 }
571 564 }
572 565
573 566 #[cfg(test)]
574 567 mod tests {
575 568 use super::*;
576 569 use pretty_assertions::assert_eq;
577 570
578 571 #[test]
579 572 fn test_path_states() {
580 573 assert_eq!(
581 574 Err(HgPathError::LeadingSlash(b"/".to_vec())),
582 575 HgPath::new(b"/").check_state()
583 576 );
584 577 assert_eq!(
585 578 Err(HgPathError::ConsecutiveSlashes {
586 579 bytes: b"a/b//c".to_vec(),
587 580 second_slash_index: 4
588 581 }),
589 582 HgPath::new(b"a/b//c").check_state()
590 583 );
591 584 assert_eq!(
592 585 Err(HgPathError::ContainsNullByte {
593 586 bytes: b"a/b/\0c".to_vec(),
594 587 null_byte_index: 4
595 588 }),
596 589 HgPath::new(b"a/b/\0c").check_state()
597 590 );
598 591 // TODO test HgPathError::DecodeError for the Windows implementation.
599 592 assert_eq!(true, HgPath::new(b"").is_valid());
600 593 assert_eq!(true, HgPath::new(b"a/b/c").is_valid());
601 594 // Backslashes in paths are not significant, but allowed
602 595 assert_eq!(true, HgPath::new(br"a\b/c").is_valid());
603 596 // Dots in paths are not significant, but allowed
604 597 assert_eq!(true, HgPath::new(b"a/b/../c/").is_valid());
605 598 assert_eq!(true, HgPath::new(b"./a/b/../c/").is_valid());
606 599 }
607 600
608 601 #[test]
609 602 fn test_iter() {
610 603 let path = HgPath::new(b"a");
611 604 let mut iter = path.bytes();
612 605 assert_eq!(Some(&b'a'), iter.next());
613 606 assert_eq!(None, iter.next_back());
614 607 assert_eq!(None, iter.next());
615 608
616 609 let path = HgPath::new(b"a");
617 610 let mut iter = path.bytes();
618 611 assert_eq!(Some(&b'a'), iter.next_back());
619 612 assert_eq!(None, iter.next_back());
620 613 assert_eq!(None, iter.next());
621 614
622 615 let path = HgPath::new(b"abc");
623 616 let mut iter = path.bytes();
624 617 assert_eq!(Some(&b'a'), iter.next());
625 618 assert_eq!(Some(&b'c'), iter.next_back());
626 619 assert_eq!(Some(&b'b'), iter.next_back());
627 620 assert_eq!(None, iter.next_back());
628 621 assert_eq!(None, iter.next());
629 622
630 623 let path = HgPath::new(b"abc");
631 624 let mut iter = path.bytes();
632 625 assert_eq!(Some(&b'a'), iter.next());
633 626 assert_eq!(Some(&b'b'), iter.next());
634 627 assert_eq!(Some(&b'c'), iter.next());
635 628 assert_eq!(None, iter.next_back());
636 629 assert_eq!(None, iter.next());
637 630
638 631 let path = HgPath::new(b"abc");
639 632 let iter = path.bytes();
640 633 let mut vec = Vec::new();
641 634 vec.extend(iter);
642 635 assert_eq!(vec![b'a', b'b', b'c'], vec);
643 636
644 637 let path = HgPath::new(b"abc");
645 638 let mut iter = path.bytes();
646 639 assert_eq!(Some(2), iter.rposition(|c| *c == b'c'));
647 640
648 641 let path = HgPath::new(b"abc");
649 642 let mut iter = path.bytes();
650 643 assert_eq!(None, iter.rposition(|c| *c == b'd'));
651 644 }
652 645
653 646 #[test]
654 647 fn test_join() {
655 648 let path = HgPathBuf::from_bytes(b"a").join(HgPath::new(b"b"));
656 649 assert_eq!(b"a/b", path.as_bytes());
657 650
658 651 let path = HgPathBuf::from_bytes(b"a/").join(HgPath::new(b"b/c"));
659 652 assert_eq!(b"a/b/c", path.as_bytes());
660 653
661 654 // No leading slash if empty before join
662 655 let path = HgPathBuf::new().join(HgPath::new(b"b/c"));
663 656 assert_eq!(b"b/c", path.as_bytes());
664 657
665 658 // The leading slash is an invalid representation of an `HgPath`, but
666 659 // it can happen. This creates another invalid representation of
667 660 // consecutive bytes.
668 661 // TODO What should be done in this case? Should we silently remove
669 662 // the extra slash? Should we change the signature to a problematic
670 663 // `Result<HgPathBuf, HgPathError>`, or should we just keep it so and
671 664 // let the error happen upon filesystem interaction?
672 665 let path = HgPathBuf::from_bytes(b"a/").join(HgPath::new(b"/b"));
673 666 assert_eq!(b"a//b", path.as_bytes());
674 667 let path = HgPathBuf::from_bytes(b"a").join(HgPath::new(b"/b"));
675 668 assert_eq!(b"a//b", path.as_bytes());
676 669 }
677 670
678 671 #[test]
679 672 fn test_relative_to() {
680 673 let path = HgPath::new(b"");
681 674 let base = HgPath::new(b"");
682 675 assert_eq!(Some(path), path.relative_to(base));
683 676
684 677 let path = HgPath::new(b"path");
685 678 let base = HgPath::new(b"");
686 679 assert_eq!(Some(path), path.relative_to(base));
687 680
688 681 let path = HgPath::new(b"a");
689 682 let base = HgPath::new(b"b");
690 683 assert_eq!(None, path.relative_to(base));
691 684
692 685 let path = HgPath::new(b"a/b");
693 686 let base = HgPath::new(b"a");
694 687 assert_eq!(None, path.relative_to(base));
695 688
696 689 let path = HgPath::new(b"a/b");
697 690 let base = HgPath::new(b"a/");
698 691 assert_eq!(Some(HgPath::new(b"b")), path.relative_to(base));
699 692
700 693 let path = HgPath::new(b"nested/path/to/b");
701 694 let base = HgPath::new(b"nested/path/");
702 695 assert_eq!(Some(HgPath::new(b"to/b")), path.relative_to(base));
703 696
704 697 let path = HgPath::new(b"ends/with/dir/");
705 698 let base = HgPath::new(b"ends/");
706 699 assert_eq!(Some(HgPath::new(b"with/dir/")), path.relative_to(base));
707 700 }
708 701
709 702 #[test]
710 703 #[cfg(unix)]
711 704 fn test_split_drive() {
712 705 // Taken from the Python stdlib's tests
713 706 assert_eq!(
714 707 HgPath::new(br"/foo/bar").split_drive(),
715 708 (HgPath::new(b""), HgPath::new(br"/foo/bar"))
716 709 );
717 710 assert_eq!(
718 711 HgPath::new(br"foo:bar").split_drive(),
719 712 (HgPath::new(b""), HgPath::new(br"foo:bar"))
720 713 );
721 714 assert_eq!(
722 715 HgPath::new(br":foo:bar").split_drive(),
723 716 (HgPath::new(b""), HgPath::new(br":foo:bar"))
724 717 );
725 718 // Also try NT paths; should not split them
726 719 assert_eq!(
727 720 HgPath::new(br"c:\foo\bar").split_drive(),
728 721 (HgPath::new(b""), HgPath::new(br"c:\foo\bar"))
729 722 );
730 723 assert_eq!(
731 724 HgPath::new(b"c:/foo/bar").split_drive(),
732 725 (HgPath::new(b""), HgPath::new(br"c:/foo/bar"))
733 726 );
734 727 assert_eq!(
735 728 HgPath::new(br"\\conky\mountpoint\foo\bar").split_drive(),
736 729 (
737 730 HgPath::new(b""),
738 731 HgPath::new(br"\\conky\mountpoint\foo\bar")
739 732 )
740 733 );
741 734 }
742 735
743 736 #[test]
744 737 #[cfg(windows)]
745 738 fn test_split_drive() {
746 739 assert_eq!(
747 740 HgPath::new(br"c:\foo\bar").split_drive(),
748 741 (HgPath::new(br"c:"), HgPath::new(br"\foo\bar"))
749 742 );
750 743 assert_eq!(
751 744 HgPath::new(b"c:/foo/bar").split_drive(),
752 745 (HgPath::new(br"c:"), HgPath::new(br"/foo/bar"))
753 746 );
754 747 assert_eq!(
755 748 HgPath::new(br"\\conky\mountpoint\foo\bar").split_drive(),
756 749 (
757 750 HgPath::new(br"\\conky\mountpoint"),
758 751 HgPath::new(br"\foo\bar")
759 752 )
760 753 );
761 754 assert_eq!(
762 755 HgPath::new(br"//conky/mountpoint/foo/bar").split_drive(),
763 756 (
764 757 HgPath::new(br"//conky/mountpoint"),
765 758 HgPath::new(br"/foo/bar")
766 759 )
767 760 );
768 761 assert_eq!(
769 762 HgPath::new(br"\\\conky\mountpoint\foo\bar").split_drive(),
770 763 (
771 764 HgPath::new(br""),
772 765 HgPath::new(br"\\\conky\mountpoint\foo\bar")
773 766 )
774 767 );
775 768 assert_eq!(
776 769 HgPath::new(br"///conky/mountpoint/foo/bar").split_drive(),
777 770 (
778 771 HgPath::new(br""),
779 772 HgPath::new(br"///conky/mountpoint/foo/bar")
780 773 )
781 774 );
782 775 assert_eq!(
783 776 HgPath::new(br"\\conky\\mountpoint\foo\bar").split_drive(),
784 777 (
785 778 HgPath::new(br""),
786 779 HgPath::new(br"\\conky\\mountpoint\foo\bar")
787 780 )
788 781 );
789 782 assert_eq!(
790 783 HgPath::new(br"//conky//mountpoint/foo/bar").split_drive(),
791 784 (
792 785 HgPath::new(br""),
793 786 HgPath::new(br"//conky//mountpoint/foo/bar")
794 787 )
795 788 );
796 789 // UNC part containing U+0130
797 790 assert_eq!(
798 791 HgPath::new(b"//conky/MOUNTPO\xc4\xb0NT/foo/bar").split_drive(),
799 792 (
800 793 HgPath::new(b"//conky/MOUNTPO\xc4\xb0NT"),
801 794 HgPath::new(br"/foo/bar")
802 795 )
803 796 );
804 797 }
805 798
806 799 #[test]
807 800 fn test_parent() {
808 801 let path = HgPath::new(b"");
809 802 assert_eq!(path.parent(), path);
810 803
811 804 let path = HgPath::new(b"a");
812 805 assert_eq!(path.parent(), HgPath::new(b""));
813 806
814 807 let path = HgPath::new(b"a/b");
815 808 assert_eq!(path.parent(), HgPath::new(b"a"));
816 809
817 810 let path = HgPath::new(b"a/other/b");
818 811 assert_eq!(path.parent(), HgPath::new(b"a/other"));
819 812 }
820 813 }
@@ -1,499 +1,490 b''
1 1 // dirstate_map.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Bindings for the `hg::dirstate::dirstate_map` file provided by the
9 9 //! `hg-core` package.
10 10
11 11 use std::cell::{RefCell, RefMut};
12 12 use std::convert::TryInto;
13 13
14 14 use cpython::{
15 15 exc, PyBool, PyBytes, PyClone, PyDict, PyErr, PyList, PyNone, PyObject,
16 16 PyResult, Python, PythonObject, ToPyObject, UnsafePyLeaked,
17 17 };
18 18
19 19 use crate::{
20 20 dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
21 21 dirstate::item::DirstateItem,
22 22 pybytes_deref::PyBytesDeref,
23 23 };
24 24 use hg::{
25 25 dirstate::StateMapIter,
26 dirstate_tree::dirstate_map::DirstateMap as TreeDirstateMap,
27 26 dirstate_tree::on_disk::DirstateV2ParseError,
28 27 dirstate_tree::owning::OwningDirstateMap,
29 28 revlog::Node,
30 29 utils::files::normalize_case,
31 30 utils::hg_path::{HgPath, HgPathBuf},
32 31 DirstateEntry, DirstateError, DirstateParents, EntryState,
33 32 };
34 33
35 34 // TODO
36 35 // This object needs to share references to multiple members of its Rust
37 36 // inner struct, namely `copy_map`, `dirs` and `all_dirs`.
38 37 // Right now `CopyMap` is done, but it needs to have an explicit reference
39 38 // to `RustDirstateMap` which itself needs to have an encapsulation for
40 39 // every method in `CopyMap` (copymapcopy, etc.).
41 40 // This is ugly and hard to maintain.
42 41 // The same logic applies to `dirs` and `all_dirs`, however the `Dirs`
43 42 // `py_class!` is already implemented and does not mention
44 43 // `RustDirstateMap`, rightfully so.
45 44 // All attributes also have to have a separate refcount data attribute for
46 45 // leaks, with all methods that go along for reference sharing.
47 46 py_class!(pub class DirstateMap |py| {
48 47 @shared data inner: OwningDirstateMap;
49 48
50 49 /// Returns a `(dirstate_map, parents)` tuple
51 50 @staticmethod
52 51 def new_v1(
53 52 on_disk: PyBytes,
54 53 ) -> PyResult<PyObject> {
55 54 let on_disk = PyBytesDeref::new(py, on_disk);
56 let mut map = OwningDirstateMap::new_empty(on_disk);
57 let (on_disk, map_placeholder) = map.get_pair_mut();
58
59 let (actual_map, parents) = TreeDirstateMap::new_v1(on_disk)
55 let (map, parents) = OwningDirstateMap::new_v1(on_disk)
60 56 .map_err(|e| dirstate_error(py, e))?;
61 *map_placeholder = actual_map;
62 57 let map = Self::create_instance(py, map)?;
63 let parents = parents.map(|p| {
64 let p1 = PyBytes::new(py, p.p1.as_bytes());
65 let p2 = PyBytes::new(py, p.p2.as_bytes());
66 (p1, p2)
67 });
58 let p1 = PyBytes::new(py, parents.p1.as_bytes());
59 let p2 = PyBytes::new(py, parents.p2.as_bytes());
60 let parents = (p1, p2);
68 61 Ok((map, parents).to_py_object(py).into_object())
69 62 }
70 63
71 64 /// Returns a DirstateMap
72 65 @staticmethod
73 66 def new_v2(
74 67 on_disk: PyBytes,
75 68 data_size: usize,
76 69 tree_metadata: PyBytes,
77 70 ) -> PyResult<PyObject> {
78 71 let dirstate_error = |e: DirstateError| {
79 72 PyErr::new::<exc::OSError, _>(py, format!("Dirstate error: {:?}", e))
80 73 };
81 74 let on_disk = PyBytesDeref::new(py, on_disk);
82 let mut map = OwningDirstateMap::new_empty(on_disk);
83 let (on_disk, map_placeholder) = map.get_pair_mut();
84 *map_placeholder = TreeDirstateMap::new_v2(
75 let map = OwningDirstateMap::new_v2(
85 76 on_disk, data_size, tree_metadata.data(py),
86 77 ).map_err(dirstate_error)?;
87 78 let map = Self::create_instance(py, map)?;
88 79 Ok(map.into_object())
89 80 }
90 81
91 82 def clear(&self) -> PyResult<PyObject> {
92 83 self.inner(py).borrow_mut().clear();
93 84 Ok(py.None())
94 85 }
95 86
96 87 def get(
97 88 &self,
98 89 key: PyObject,
99 90 default: Option<PyObject> = None
100 91 ) -> PyResult<Option<PyObject>> {
101 92 let key = key.extract::<PyBytes>(py)?;
102 93 match self
103 94 .inner(py)
104 95 .borrow()
105 96 .get(HgPath::new(key.data(py)))
106 97 .map_err(|e| v2_error(py, e))?
107 98 {
108 99 Some(entry) => {
109 100 Ok(Some(DirstateItem::new_as_pyobject(py, entry)?))
110 101 },
111 102 None => Ok(default)
112 103 }
113 104 }
114 105
115 106 def set_dirstate_item(
116 107 &self,
117 108 path: PyObject,
118 109 item: DirstateItem
119 110 ) -> PyResult<PyObject> {
120 111 let f = path.extract::<PyBytes>(py)?;
121 112 let filename = HgPath::new(f.data(py));
122 113 self.inner(py)
123 114 .borrow_mut()
124 115 .set_entry(filename, item.get_entry(py))
125 116 .map_err(|e| v2_error(py, e))?;
126 117 Ok(py.None())
127 118 }
128 119
129 120 def addfile(
130 121 &self,
131 122 f: PyBytes,
132 123 item: DirstateItem,
133 124 ) -> PyResult<PyNone> {
134 125 let filename = HgPath::new(f.data(py));
135 126 let entry = item.get_entry(py);
136 127 self.inner(py)
137 128 .borrow_mut()
138 129 .add_file(filename, entry)
139 130 .map_err(|e |dirstate_error(py, e))?;
140 131 Ok(PyNone)
141 132 }
142 133
143 134 def removefile(
144 135 &self,
145 136 f: PyObject,
146 137 in_merge: PyObject
147 138 ) -> PyResult<PyObject> {
148 139 self.inner(py).borrow_mut()
149 140 .remove_file(
150 141 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
151 142 in_merge.extract::<PyBool>(py)?.is_true(),
152 143 )
153 144 .or_else(|_| {
154 145 Err(PyErr::new::<exc::OSError, _>(
155 146 py,
156 147 "Dirstate error".to_string(),
157 148 ))
158 149 })?;
159 150 Ok(py.None())
160 151 }
161 152
162 153 def drop_item_and_copy_source(
163 154 &self,
164 155 f: PyBytes,
165 156 ) -> PyResult<PyNone> {
166 157 self.inner(py)
167 158 .borrow_mut()
168 159 .drop_entry_and_copy_source(HgPath::new(f.data(py)))
169 160 .map_err(|e |dirstate_error(py, e))?;
170 161 Ok(PyNone)
171 162 }
172 163
173 164 def hastrackeddir(&self, d: PyObject) -> PyResult<PyBool> {
174 165 let d = d.extract::<PyBytes>(py)?;
175 166 Ok(self.inner(py).borrow_mut()
176 167 .has_tracked_dir(HgPath::new(d.data(py)))
177 168 .map_err(|e| {
178 169 PyErr::new::<exc::ValueError, _>(py, e.to_string())
179 170 })?
180 171 .to_py_object(py))
181 172 }
182 173
183 174 def hasdir(&self, d: PyObject) -> PyResult<PyBool> {
184 175 let d = d.extract::<PyBytes>(py)?;
185 176 Ok(self.inner(py).borrow_mut()
186 177 .has_dir(HgPath::new(d.data(py)))
187 178 .map_err(|e| {
188 179 PyErr::new::<exc::ValueError, _>(py, e.to_string())
189 180 })?
190 181 .to_py_object(py))
191 182 }
192 183
193 184 def write_v1(
194 185 &self,
195 186 p1: PyObject,
196 187 p2: PyObject,
197 188 ) -> PyResult<PyBytes> {
198 189 let inner = self.inner(py).borrow();
199 190 let parents = DirstateParents {
200 191 p1: extract_node_id(py, &p1)?,
201 192 p2: extract_node_id(py, &p2)?,
202 193 };
203 194 let result = inner.pack_v1(parents);
204 195 match result {
205 196 Ok(packed) => Ok(PyBytes::new(py, &packed)),
206 197 Err(_) => Err(PyErr::new::<exc::OSError, _>(
207 198 py,
208 199 "Dirstate error".to_string(),
209 200 )),
210 201 }
211 202 }
212 203
213 204 /// Returns new data together with whether that data should be appended to
214 205 /// the existing data file whose content is at `self.on_disk` (True),
215 206 /// instead of written to a new data file (False).
216 207 def write_v2(
217 208 &self,
218 209 can_append: bool,
219 210 ) -> PyResult<PyObject> {
220 211 let inner = self.inner(py).borrow();
221 212 let result = inner.pack_v2(can_append);
222 213 match result {
223 214 Ok((packed, tree_metadata, append)) => {
224 215 let packed = PyBytes::new(py, &packed);
225 216 let tree_metadata = PyBytes::new(py, tree_metadata.as_bytes());
226 217 let tuple = (packed, tree_metadata, append);
227 218 Ok(tuple.to_py_object(py).into_object())
228 219 },
229 220 Err(_) => Err(PyErr::new::<exc::OSError, _>(
230 221 py,
231 222 "Dirstate error".to_string(),
232 223 )),
233 224 }
234 225 }
235 226
236 227 def filefoldmapasdict(&self) -> PyResult<PyDict> {
237 228 let dict = PyDict::new(py);
238 229 for item in self.inner(py).borrow_mut().iter() {
239 230 let (path, entry) = item.map_err(|e| v2_error(py, e))?;
240 231 if entry.state() != EntryState::Removed {
241 232 let key = normalize_case(path);
242 233 let value = path;
243 234 dict.set_item(
244 235 py,
245 236 PyBytes::new(py, key.as_bytes()).into_object(),
246 237 PyBytes::new(py, value.as_bytes()).into_object(),
247 238 )?;
248 239 }
249 240 }
250 241 Ok(dict)
251 242 }
252 243
253 244 def __len__(&self) -> PyResult<usize> {
254 245 Ok(self.inner(py).borrow().len())
255 246 }
256 247
257 248 def __contains__(&self, key: PyObject) -> PyResult<bool> {
258 249 let key = key.extract::<PyBytes>(py)?;
259 250 self.inner(py)
260 251 .borrow()
261 252 .contains_key(HgPath::new(key.data(py)))
262 253 .map_err(|e| v2_error(py, e))
263 254 }
264 255
265 256 def __getitem__(&self, key: PyObject) -> PyResult<PyObject> {
266 257 let key = key.extract::<PyBytes>(py)?;
267 258 let key = HgPath::new(key.data(py));
268 259 match self
269 260 .inner(py)
270 261 .borrow()
271 262 .get(key)
272 263 .map_err(|e| v2_error(py, e))?
273 264 {
274 265 Some(entry) => {
275 266 Ok(DirstateItem::new_as_pyobject(py, entry)?)
276 267 },
277 268 None => Err(PyErr::new::<exc::KeyError, _>(
278 269 py,
279 270 String::from_utf8_lossy(key.as_bytes()),
280 271 )),
281 272 }
282 273 }
283 274
284 275 def keys(&self) -> PyResult<DirstateMapKeysIterator> {
285 276 let leaked_ref = self.inner(py).leak_immutable();
286 277 DirstateMapKeysIterator::from_inner(
287 278 py,
288 279 unsafe { leaked_ref.map(py, |o| o.iter()) },
289 280 )
290 281 }
291 282
292 283 def items(&self) -> PyResult<DirstateMapItemsIterator> {
293 284 let leaked_ref = self.inner(py).leak_immutable();
294 285 DirstateMapItemsIterator::from_inner(
295 286 py,
296 287 unsafe { leaked_ref.map(py, |o| o.iter()) },
297 288 )
298 289 }
299 290
300 291 def __iter__(&self) -> PyResult<DirstateMapKeysIterator> {
301 292 let leaked_ref = self.inner(py).leak_immutable();
302 293 DirstateMapKeysIterator::from_inner(
303 294 py,
304 295 unsafe { leaked_ref.map(py, |o| o.iter()) },
305 296 )
306 297 }
307 298
308 299 // TODO all copymap* methods, see docstring above
309 300 def copymapcopy(&self) -> PyResult<PyDict> {
310 301 let dict = PyDict::new(py);
311 302 for item in self.inner(py).borrow().copy_map_iter() {
312 303 let (key, value) = item.map_err(|e| v2_error(py, e))?;
313 304 dict.set_item(
314 305 py,
315 306 PyBytes::new(py, key.as_bytes()),
316 307 PyBytes::new(py, value.as_bytes()),
317 308 )?;
318 309 }
319 310 Ok(dict)
320 311 }
321 312
322 313 def copymapgetitem(&self, key: PyObject) -> PyResult<PyBytes> {
323 314 let key = key.extract::<PyBytes>(py)?;
324 315 match self
325 316 .inner(py)
326 317 .borrow()
327 318 .copy_map_get(HgPath::new(key.data(py)))
328 319 .map_err(|e| v2_error(py, e))?
329 320 {
330 321 Some(copy) => Ok(PyBytes::new(py, copy.as_bytes())),
331 322 None => Err(PyErr::new::<exc::KeyError, _>(
332 323 py,
333 324 String::from_utf8_lossy(key.data(py)),
334 325 )),
335 326 }
336 327 }
337 328 def copymap(&self) -> PyResult<CopyMap> {
338 329 CopyMap::from_inner(py, self.clone_ref(py))
339 330 }
340 331
341 332 def copymaplen(&self) -> PyResult<usize> {
342 333 Ok(self.inner(py).borrow().copy_map_len())
343 334 }
344 335 def copymapcontains(&self, key: PyObject) -> PyResult<bool> {
345 336 let key = key.extract::<PyBytes>(py)?;
346 337 self.inner(py)
347 338 .borrow()
348 339 .copy_map_contains_key(HgPath::new(key.data(py)))
349 340 .map_err(|e| v2_error(py, e))
350 341 }
351 342 def copymapget(
352 343 &self,
353 344 key: PyObject,
354 345 default: Option<PyObject>
355 346 ) -> PyResult<Option<PyObject>> {
356 347 let key = key.extract::<PyBytes>(py)?;
357 348 match self
358 349 .inner(py)
359 350 .borrow()
360 351 .copy_map_get(HgPath::new(key.data(py)))
361 352 .map_err(|e| v2_error(py, e))?
362 353 {
363 354 Some(copy) => Ok(Some(
364 355 PyBytes::new(py, copy.as_bytes()).into_object(),
365 356 )),
366 357 None => Ok(default),
367 358 }
368 359 }
369 360 def copymapsetitem(
370 361 &self,
371 362 key: PyObject,
372 363 value: PyObject
373 364 ) -> PyResult<PyObject> {
374 365 let key = key.extract::<PyBytes>(py)?;
375 366 let value = value.extract::<PyBytes>(py)?;
376 367 self.inner(py)
377 368 .borrow_mut()
378 369 .copy_map_insert(
379 370 HgPathBuf::from_bytes(key.data(py)),
380 371 HgPathBuf::from_bytes(value.data(py)),
381 372 )
382 373 .map_err(|e| v2_error(py, e))?;
383 374 Ok(py.None())
384 375 }
385 376 def copymappop(
386 377 &self,
387 378 key: PyObject,
388 379 default: Option<PyObject>
389 380 ) -> PyResult<Option<PyObject>> {
390 381 let key = key.extract::<PyBytes>(py)?;
391 382 match self
392 383 .inner(py)
393 384 .borrow_mut()
394 385 .copy_map_remove(HgPath::new(key.data(py)))
395 386 .map_err(|e| v2_error(py, e))?
396 387 {
397 388 Some(copy) => Ok(Some(
398 389 PyBytes::new(py, copy.as_bytes()).into_object(),
399 390 )),
400 391 None => Ok(default),
401 392 }
402 393 }
403 394
404 395 def copymapiter(&self) -> PyResult<CopyMapKeysIterator> {
405 396 let leaked_ref = self.inner(py).leak_immutable();
406 397 CopyMapKeysIterator::from_inner(
407 398 py,
408 399 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
409 400 )
410 401 }
411 402
412 403 def copymapitemsiter(&self) -> PyResult<CopyMapItemsIterator> {
413 404 let leaked_ref = self.inner(py).leak_immutable();
414 405 CopyMapItemsIterator::from_inner(
415 406 py,
416 407 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
417 408 )
418 409 }
419 410
420 411 def tracked_dirs(&self) -> PyResult<PyList> {
421 412 let dirs = PyList::new(py, &[]);
422 413 for path in self.inner(py).borrow_mut().iter_tracked_dirs()
423 414 .map_err(|e |dirstate_error(py, e))?
424 415 {
425 416 let path = path.map_err(|e| v2_error(py, e))?;
426 417 let path = PyBytes::new(py, path.as_bytes());
427 418 dirs.append(py, path.into_object())
428 419 }
429 420 Ok(dirs)
430 421 }
431 422
432 423 def debug_iter(&self, all: bool) -> PyResult<PyList> {
433 424 let dirs = PyList::new(py, &[]);
434 425 for item in self.inner(py).borrow().debug_iter(all) {
435 426 let (path, (state, mode, size, mtime)) =
436 427 item.map_err(|e| v2_error(py, e))?;
437 428 let path = PyBytes::new(py, path.as_bytes());
438 429 let item = (path, state, mode, size, mtime);
439 430 dirs.append(py, item.to_py_object(py).into_object())
440 431 }
441 432 Ok(dirs)
442 433 }
443 434 });
444 435
445 436 impl DirstateMap {
446 437 pub fn get_inner_mut<'a>(
447 438 &'a self,
448 439 py: Python<'a>,
449 440 ) -> RefMut<'a, OwningDirstateMap> {
450 441 self.inner(py).borrow_mut()
451 442 }
452 443 fn translate_key(
453 444 py: Python,
454 445 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
455 446 ) -> PyResult<Option<PyBytes>> {
456 447 let (f, _entry) = res.map_err(|e| v2_error(py, e))?;
457 448 Ok(Some(PyBytes::new(py, f.as_bytes())))
458 449 }
459 450 fn translate_key_value(
460 451 py: Python,
461 452 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
462 453 ) -> PyResult<Option<(PyBytes, PyObject)>> {
463 454 let (f, entry) = res.map_err(|e| v2_error(py, e))?;
464 455 Ok(Some((
465 456 PyBytes::new(py, f.as_bytes()),
466 457 DirstateItem::new_as_pyobject(py, entry)?,
467 458 )))
468 459 }
469 460 }
470 461
471 462 py_shared_iterator!(
472 463 DirstateMapKeysIterator,
473 464 UnsafePyLeaked<StateMapIter<'static>>,
474 465 DirstateMap::translate_key,
475 466 Option<PyBytes>
476 467 );
477 468
478 469 py_shared_iterator!(
479 470 DirstateMapItemsIterator,
480 471 UnsafePyLeaked<StateMapIter<'static>>,
481 472 DirstateMap::translate_key_value,
482 473 Option<(PyBytes, PyObject)>
483 474 );
484 475
485 476 fn extract_node_id(py: Python, obj: &PyObject) -> PyResult<Node> {
486 477 let bytes = obj.extract::<PyBytes>(py)?;
487 478 match bytes.data(py).try_into() {
488 479 Ok(s) => Ok(s),
489 480 Err(e) => Err(PyErr::new::<exc::ValueError, _>(py, e.to_string())),
490 481 }
491 482 }
492 483
493 484 pub(super) fn v2_error(py: Python<'_>, _: DirstateV2ParseError) -> PyErr {
494 485 PyErr::new::<exc::ValueError, _>(py, "corrupted dirstate-v2")
495 486 }
496 487
497 488 fn dirstate_error(py: Python<'_>, e: DirstateError) -> PyErr {
498 489 PyErr::new::<exc::OSError, _>(py, format!("Dirstate error: {:?}", e))
499 490 }
@@ -1,303 +1,302 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Bindings for the `hg::status` module provided by the
9 9 //! `hg-core` crate. From Python, this will be seen as
10 10 //! `rustext.dirstate.status`.
11 11
12 12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
13 13 use cpython::{
14 14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
15 15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
16 16 };
17 17 use hg::dirstate::status::StatusPath;
18 18 use hg::{
19 19 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
20 20 parse_pattern_syntax,
21 21 utils::{
22 22 files::{get_bytes_from_path, get_path_from_bytes},
23 23 hg_path::{HgPath, HgPathBuf},
24 24 },
25 25 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
26 26 StatusOptions,
27 27 };
28 28 use std::borrow::Borrow;
29 29
30 30 fn collect_status_path_list(py: Python, paths: &[StatusPath<'_>]) -> PyList {
31 31 collect_pybytes_list(py, paths.iter().map(|item| &*item.path))
32 32 }
33 33
34 34 /// This will be useless once trait impls for collection are added to `PyBytes`
35 35 /// upstream.
36 36 fn collect_pybytes_list(
37 37 py: Python,
38 38 iter: impl Iterator<Item = impl AsRef<HgPath>>,
39 39 ) -> PyList {
40 40 let list = PyList::new(py, &[]);
41 41
42 42 for path in iter {
43 43 list.append(
44 44 py,
45 45 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
46 46 )
47 47 }
48 48
49 49 list
50 50 }
51 51
52 52 fn collect_bad_matches(
53 53 py: Python,
54 54 collection: &[(impl AsRef<HgPath>, BadMatch)],
55 55 ) -> PyResult<PyList> {
56 56 let list = PyList::new(py, &[]);
57 57
58 58 let os = py.import("os")?;
59 59 let get_error_message = |code: i32| -> PyResult<_> {
60 60 os.call(
61 61 py,
62 62 "strerror",
63 63 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
64 64 None,
65 65 )
66 66 };
67 67
68 68 for (path, bad_match) in collection.iter() {
69 69 let message = match bad_match {
70 70 BadMatch::OsError(code) => get_error_message(*code)?,
71 71 BadMatch::BadType(bad_type) => format!(
72 72 "unsupported file type (type is {})",
73 73 bad_type.to_string()
74 74 )
75 75 .to_py_object(py)
76 76 .into_object(),
77 77 };
78 78 list.append(
79 79 py,
80 80 (PyBytes::new(py, path.as_ref().as_bytes()), message)
81 81 .to_py_object(py)
82 82 .into_object(),
83 83 )
84 84 }
85 85
86 86 Ok(list)
87 87 }
88 88
89 89 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
90 90 match err {
91 91 StatusError::Pattern(e) => {
92 92 let as_string = e.to_string();
93 93 log::trace!("Rust status fallback: `{}`", &as_string);
94 94
95 95 PyErr::new::<FallbackError, _>(py, &as_string)
96 96 }
97 97 e => PyErr::new::<ValueError, _>(py, e.to_string()),
98 98 }
99 99 }
100 100
101 101 pub fn status_wrapper(
102 102 py: Python,
103 103 dmap: DirstateMap,
104 104 matcher: PyObject,
105 105 root_dir: PyObject,
106 106 ignore_files: PyList,
107 107 check_exec: bool,
108 108 list_clean: bool,
109 109 list_ignored: bool,
110 110 list_unknown: bool,
111 111 collect_traversed_dirs: bool,
112 112 ) -> PyResult<PyTuple> {
113 113 let bytes = root_dir.extract::<PyBytes>(py)?;
114 114 let root_dir = get_path_from_bytes(bytes.data(py));
115 115
116 116 let dmap: DirstateMap = dmap.to_py_object(py);
117 117 let mut dmap = dmap.get_inner_mut(py);
118 118
119 119 let ignore_files: PyResult<Vec<_>> = ignore_files
120 120 .iter(py)
121 121 .map(|b| {
122 122 let file = b.extract::<PyBytes>(py)?;
123 123 Ok(get_path_from_bytes(file.data(py)).to_owned())
124 124 })
125 125 .collect();
126 126 let ignore_files = ignore_files?;
127 127 // The caller may call `copymap.items()` separately
128 128 let list_copies = false;
129 129
130 let after_status = |res: Result<(DirstateStatus<'_>, _), StatusError>| {
131 let (status_res, warnings) =
132 res.map_err(|e| handle_fallback(py, e))?;
133 build_response(py, status_res, warnings)
134 };
135
130 136 match matcher.get_type(py).name(py).borrow() {
131 137 "alwaysmatcher" => {
132 138 let matcher = AlwaysMatcher;
133 let (status_res, warnings) = dmap
134 .status(
135 &matcher,
136 root_dir.to_path_buf(),
137 ignore_files,
138 StatusOptions {
139 check_exec,
140 list_clean,
141 list_ignored,
142 list_unknown,
143 list_copies,
144 collect_traversed_dirs,
145 },
146 )
147 .map_err(|e| handle_fallback(py, e))?;
148 build_response(py, status_res, warnings)
139 dmap.with_status(
140 &matcher,
141 root_dir.to_path_buf(),
142 ignore_files,
143 StatusOptions {
144 check_exec,
145 list_clean,
146 list_ignored,
147 list_unknown,
148 list_copies,
149 collect_traversed_dirs,
150 },
151 after_status,
152 )
149 153 }
150 154 "exactmatcher" => {
151 155 let files = matcher.call_method(
152 156 py,
153 157 "files",
154 158 PyTuple::new(py, &[]),
155 159 None,
156 160 )?;
157 161 let files: PyList = files.cast_into(py)?;
158 162 let files: PyResult<Vec<HgPathBuf>> = files
159 163 .iter(py)
160 164 .map(|f| {
161 165 Ok(HgPathBuf::from_bytes(
162 166 f.extract::<PyBytes>(py)?.data(py),
163 167 ))
164 168 })
165 169 .collect();
166 170
167 171 let files = files?;
168 172 let matcher = FileMatcher::new(files.as_ref())
169 173 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
170 let (status_res, warnings) = dmap
171 .status(
172 &matcher,
173 root_dir.to_path_buf(),
174 ignore_files,
175 StatusOptions {
176 check_exec,
177 list_clean,
178 list_ignored,
179 list_unknown,
180 list_copies,
181 collect_traversed_dirs,
182 },
183 )
184 .map_err(|e| handle_fallback(py, e))?;
185 build_response(py, status_res, warnings)
174 dmap.with_status(
175 &matcher,
176 root_dir.to_path_buf(),
177 ignore_files,
178 StatusOptions {
179 check_exec,
180 list_clean,
181 list_ignored,
182 list_unknown,
183 list_copies,
184 collect_traversed_dirs,
185 },
186 after_status,
187 )
186 188 }
187 189 "includematcher" => {
188 190 // Get the patterns from Python even though most of them are
189 191 // redundant with those we will parse later on, as they include
190 192 // those passed from the command line.
191 193 let ignore_patterns: PyResult<Vec<_>> = matcher
192 194 .getattr(py, "_kindpats")?
193 195 .iter(py)?
194 196 .map(|k| {
195 197 let k = k?;
196 198 let syntax = parse_pattern_syntax(
197 199 &[
198 200 k.get_item(py, 0)?
199 201 .extract::<PyBytes>(py)?
200 202 .data(py),
201 203 &b":"[..],
202 204 ]
203 205 .concat(),
204 206 )
205 207 .map_err(|e| {
206 208 handle_fallback(py, StatusError::Pattern(e))
207 209 })?;
208 210 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
209 211 let pattern = pattern.data(py);
210 212 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
211 213 let source = get_path_from_bytes(source.data(py));
212 214 let new = IgnorePattern::new(syntax, pattern, source);
213 215 Ok(new)
214 216 })
215 217 .collect();
216 218
217 219 let ignore_patterns = ignore_patterns?;
218 220
219 221 let matcher = IncludeMatcher::new(ignore_patterns)
220 222 .map_err(|e| handle_fallback(py, e.into()))?;
221 223
222 let (status_res, warnings) = dmap
223 .status(
224 &matcher,
225 root_dir.to_path_buf(),
226 ignore_files,
227 StatusOptions {
228 check_exec,
229 list_clean,
230 list_ignored,
231 list_unknown,
232 list_copies,
233 collect_traversed_dirs,
234 },
235 )
236 .map_err(|e| handle_fallback(py, e))?;
237
238 build_response(py, status_res, warnings)
224 dmap.with_status(
225 &matcher,
226 root_dir.to_path_buf(),
227 ignore_files,
228 StatusOptions {
229 check_exec,
230 list_clean,
231 list_ignored,
232 list_unknown,
233 list_copies,
234 collect_traversed_dirs,
235 },
236 after_status,
237 )
239 238 }
240 239 e => Err(PyErr::new::<ValueError, _>(
241 240 py,
242 241 format!("Unsupported matcher {}", e),
243 242 )),
244 243 }
245 244 }
246 245
247 246 fn build_response(
248 247 py: Python,
249 248 status_res: DirstateStatus,
250 249 warnings: Vec<PatternFileWarning>,
251 250 ) -> PyResult<PyTuple> {
252 251 let modified = collect_status_path_list(py, &status_res.modified);
253 252 let added = collect_status_path_list(py, &status_res.added);
254 253 let removed = collect_status_path_list(py, &status_res.removed);
255 254 let deleted = collect_status_path_list(py, &status_res.deleted);
256 255 let clean = collect_status_path_list(py, &status_res.clean);
257 256 let ignored = collect_status_path_list(py, &status_res.ignored);
258 257 let unknown = collect_status_path_list(py, &status_res.unknown);
259 258 let unsure = collect_status_path_list(py, &status_res.unsure);
260 259 let bad = collect_bad_matches(py, &status_res.bad)?;
261 260 let traversed = collect_pybytes_list(py, status_res.traversed.iter());
262 261 let dirty = status_res.dirty.to_py_object(py);
263 262 let py_warnings = PyList::new(py, &[]);
264 263 for warning in warnings.iter() {
265 264 // We use duck-typing on the Python side for dispatch, good enough for
266 265 // now.
267 266 match warning {
268 267 PatternFileWarning::InvalidSyntax(file, syn) => {
269 268 py_warnings.append(
270 269 py,
271 270 (
272 271 PyBytes::new(py, &get_bytes_from_path(&file)),
273 272 PyBytes::new(py, syn),
274 273 )
275 274 .to_py_object(py)
276 275 .into_object(),
277 276 );
278 277 }
279 278 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
280 279 py,
281 280 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
282 281 ),
283 282 }
284 283 }
285 284
286 285 Ok(PyTuple::new(
287 286 py,
288 287 &[
289 288 unsure.into_object(),
290 289 modified.into_object(),
291 290 added.into_object(),
292 291 removed.into_object(),
293 292 deleted.into_object(),
294 293 clean.into_object(),
295 294 ignored.into_object(),
296 295 unknown.into_object(),
297 296 py_warnings.into_object(),
298 297 bad.into_object(),
299 298 traversed.into_object(),
300 299 dirty.into_object(),
301 300 ][..],
302 301 ))
303 302 }
@@ -1,531 +1,549 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use crate::error::CommandError;
9 9 use crate::ui::Ui;
10 10 use crate::utils::path_utils::RelativizePaths;
11 11 use clap::{Arg, SubCommand};
12 12 use format_bytes::format_bytes;
13 13 use hg;
14 14 use hg::config::Config;
15 15 use hg::dirstate::has_exec_bit;
16 16 use hg::dirstate::status::StatusPath;
17 17 use hg::dirstate::TruncatedTimestamp;
18 18 use hg::dirstate::RANGE_MASK_31BIT;
19 19 use hg::errors::{HgError, IoResultExt};
20 20 use hg::lock::LockError;
21 21 use hg::manifest::Manifest;
22 22 use hg::matchers::AlwaysMatcher;
23 23 use hg::repo::Repo;
24 24 use hg::utils::files::get_bytes_from_os_string;
25 25 use hg::utils::files::get_bytes_from_path;
26 26 use hg::utils::files::get_path_from_bytes;
27 27 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
28 use hg::DirstateStatus;
29 use hg::PatternFileWarning;
30 use hg::StatusError;
28 31 use hg::StatusOptions;
29 32 use log::info;
30 33 use std::io;
31 34 use std::path::PathBuf;
32 35
33 36 pub const HELP_TEXT: &str = "
34 37 Show changed files in the working directory
35 38
36 39 This is a pure Rust version of `hg status`.
37 40
38 41 Some options might be missing, check the list below.
39 42 ";
40 43
41 44 pub fn args() -> clap::App<'static, 'static> {
42 45 SubCommand::with_name("status")
43 46 .alias("st")
44 47 .about(HELP_TEXT)
45 48 .arg(
46 49 Arg::with_name("all")
47 50 .help("show status of all files")
48 51 .short("-A")
49 52 .long("--all"),
50 53 )
51 54 .arg(
52 55 Arg::with_name("modified")
53 56 .help("show only modified files")
54 57 .short("-m")
55 58 .long("--modified"),
56 59 )
57 60 .arg(
58 61 Arg::with_name("added")
59 62 .help("show only added files")
60 63 .short("-a")
61 64 .long("--added"),
62 65 )
63 66 .arg(
64 67 Arg::with_name("removed")
65 68 .help("show only removed files")
66 69 .short("-r")
67 70 .long("--removed"),
68 71 )
69 72 .arg(
70 73 Arg::with_name("clean")
71 74 .help("show only clean files")
72 75 .short("-c")
73 76 .long("--clean"),
74 77 )
75 78 .arg(
76 79 Arg::with_name("deleted")
77 80 .help("show only deleted files")
78 81 .short("-d")
79 82 .long("--deleted"),
80 83 )
81 84 .arg(
82 85 Arg::with_name("unknown")
83 86 .help("show only unknown (not tracked) files")
84 87 .short("-u")
85 88 .long("--unknown"),
86 89 )
87 90 .arg(
88 91 Arg::with_name("ignored")
89 92 .help("show only ignored files")
90 93 .short("-i")
91 94 .long("--ignored"),
92 95 )
93 96 .arg(
94 97 Arg::with_name("copies")
95 98 .help("show source of copied files (DEFAULT: ui.statuscopies)")
96 99 .short("-C")
97 100 .long("--copies"),
98 101 )
99 102 .arg(
100 103 Arg::with_name("no-status")
101 104 .help("hide status prefix")
102 105 .short("-n")
103 106 .long("--no-status"),
104 107 )
105 108 }
106 109
107 110 /// Pure data type allowing the caller to specify file states to display
108 111 #[derive(Copy, Clone, Debug)]
109 112 pub struct DisplayStates {
110 113 pub modified: bool,
111 114 pub added: bool,
112 115 pub removed: bool,
113 116 pub clean: bool,
114 117 pub deleted: bool,
115 118 pub unknown: bool,
116 119 pub ignored: bool,
117 120 }
118 121
119 122 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
120 123 modified: true,
121 124 added: true,
122 125 removed: true,
123 126 clean: false,
124 127 deleted: true,
125 128 unknown: true,
126 129 ignored: false,
127 130 };
128 131
129 132 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
130 133 modified: true,
131 134 added: true,
132 135 removed: true,
133 136 clean: true,
134 137 deleted: true,
135 138 unknown: true,
136 139 ignored: true,
137 140 };
138 141
139 142 impl DisplayStates {
140 143 pub fn is_empty(&self) -> bool {
141 144 !(self.modified
142 145 || self.added
143 146 || self.removed
144 147 || self.clean
145 148 || self.deleted
146 149 || self.unknown
147 150 || self.ignored)
148 151 }
149 152 }
150 153
151 154 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
152 155 // TODO: lift these limitations
153 156 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
154 157 return Err(CommandError::unsupported(
155 158 "ui.tweakdefaults is not yet supported with rhg status",
156 159 ));
157 160 }
158 161 if invocation.config.get_bool(b"ui", b"statuscopies")? {
159 162 return Err(CommandError::unsupported(
160 163 "ui.statuscopies is not yet supported with rhg status",
161 164 ));
162 165 }
163 166 if invocation
164 167 .config
165 168 .get(b"commands", b"status.terse")
166 169 .is_some()
167 170 {
168 171 return Err(CommandError::unsupported(
169 172 "status.terse is not yet supported with rhg status",
170 173 ));
171 174 }
172 175
173 176 let ui = invocation.ui;
174 177 let config = invocation.config;
175 178 let args = invocation.subcommand_args;
176 179
177 180 let verbose = !ui.plain(None)
178 181 && !args.is_present("print0")
179 182 && (config.get_bool(b"ui", b"verbose")?
180 183 || config.get_bool(b"commands", b"status.verbose")?);
181 184 if verbose {
182 185 return Err(CommandError::unsupported(
183 186 "verbose status is not supported yet",
184 187 ));
185 188 }
186 189
187 190 let all = args.is_present("all");
188 191 let display_states = if all {
189 192 // TODO when implementing `--quiet`: it excludes clean files
190 193 // from `--all`
191 194 ALL_DISPLAY_STATES
192 195 } else {
193 196 let requested = DisplayStates {
194 197 modified: args.is_present("modified"),
195 198 added: args.is_present("added"),
196 199 removed: args.is_present("removed"),
197 200 clean: args.is_present("clean"),
198 201 deleted: args.is_present("deleted"),
199 202 unknown: args.is_present("unknown"),
200 203 ignored: args.is_present("ignored"),
201 204 };
202 205 if requested.is_empty() {
203 206 DEFAULT_DISPLAY_STATES
204 207 } else {
205 208 requested
206 209 }
207 210 };
208 211 let no_status = args.is_present("no-status");
209 212 let list_copies = all
210 213 || args.is_present("copies")
211 214 || config.get_bool(b"ui", b"statuscopies")?;
212 215
213 216 let repo = invocation.repo?;
214 217
215 218 if repo.has_sparse() || repo.has_narrow() {
216 219 return Err(CommandError::unsupported(
217 220 "rhg status is not supported for sparse checkouts or narrow clones yet"
218 221 ));
219 222 }
220 223
221 224 let mut dmap = repo.dirstate_map_mut()?;
222 225
223 226 let options = StatusOptions {
224 227 // we're currently supporting file systems with exec flags only
225 228 // anyway
226 229 check_exec: true,
227 230 list_clean: display_states.clean,
228 231 list_unknown: display_states.unknown,
229 232 list_ignored: display_states.ignored,
230 233 list_copies,
231 234 collect_traversed_dirs: false,
232 235 };
233 let (mut ds_status, pattern_warnings) = dmap.status(
234 &AlwaysMatcher,
235 repo.working_directory_path().to_owned(),
236 ignore_files(repo, config),
237 options,
238 )?;
239 for warning in pattern_warnings {
240 match warning {
241 hg::PatternFileWarning::InvalidSyntax(path, syntax) => ui
242 .write_stderr(&format_bytes!(
243 b"{}: ignoring invalid syntax '{}'\n",
244 get_bytes_from_path(path),
245 &*syntax
246 ))?,
247 hg::PatternFileWarning::NoSuchFile(path) => {
248 let path = if let Ok(relative) =
249 path.strip_prefix(repo.working_directory_path())
250 {
251 relative
252 } else {
253 &*path
254 };
255 ui.write_stderr(&format_bytes!(
256 b"skipping unreadable pattern file '{}': \
257 No such file or directory\n",
258 get_bytes_from_path(path),
259 ))?
236
237 type StatusResult<'a> =
238 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
239
240 let after_status = |res: StatusResult| -> Result<_, CommandError> {
241 let (mut ds_status, pattern_warnings) = res?;
242 for warning in pattern_warnings {
243 match warning {
244 hg::PatternFileWarning::InvalidSyntax(path, syntax) => ui
245 .write_stderr(&format_bytes!(
246 b"{}: ignoring invalid syntax '{}'\n",
247 get_bytes_from_path(path),
248 &*syntax
249 ))?,
250 hg::PatternFileWarning::NoSuchFile(path) => {
251 let path = if let Ok(relative) =
252 path.strip_prefix(repo.working_directory_path())
253 {
254 relative
255 } else {
256 &*path
257 };
258 ui.write_stderr(&format_bytes!(
259 b"skipping unreadable pattern file '{}': \
260 No such file or directory\n",
261 get_bytes_from_path(path),
262 ))?
263 }
260 264 }
261 265 }
262 }
263 266
264 for (path, error) in ds_status.bad {
265 let error = match error {
266 hg::BadMatch::OsError(code) => {
267 std::io::Error::from_raw_os_error(code).to_string()
268 }
269 hg::BadMatch::BadType(ty) => {
270 format!("unsupported file type (type is {})", ty)
271 }
272 };
273 ui.write_stderr(&format_bytes!(
274 b"{}: {}\n",
275 path.as_bytes(),
276 error.as_bytes()
277 ))?
278 }
279 if !ds_status.unsure.is_empty() {
280 info!(
281 "Files to be rechecked by retrieval from filelog: {:?}",
282 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
283 );
284 }
285 let mut fixup = Vec::new();
286 if !ds_status.unsure.is_empty()
287 && (display_states.modified || display_states.clean)
288 {
289 let p1 = repo.dirstate_parents()?.p1;
290 let manifest = repo.manifest_for_node(p1).map_err(|e| {
291 CommandError::from((e, &*format!("{:x}", p1.short())))
292 })?;
293 for to_check in ds_status.unsure {
294 if unsure_is_modified(repo, &manifest, &to_check.path)? {
295 if display_states.modified {
296 ds_status.modified.push(to_check);
267 for (path, error) in ds_status.bad {
268 let error = match error {
269 hg::BadMatch::OsError(code) => {
270 std::io::Error::from_raw_os_error(code).to_string()
271 }
272 hg::BadMatch::BadType(ty) => {
273 format!("unsupported file type (type is {})", ty)
297 274 }
298 } else {
299 if display_states.clean {
300 ds_status.clean.push(to_check.clone());
275 };
276 ui.write_stderr(&format_bytes!(
277 b"{}: {}\n",
278 path.as_bytes(),
279 error.as_bytes()
280 ))?
281 }
282 if !ds_status.unsure.is_empty() {
283 info!(
284 "Files to be rechecked by retrieval from filelog: {:?}",
285 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
286 );
287 }
288 let mut fixup = Vec::new();
289 if !ds_status.unsure.is_empty()
290 && (display_states.modified || display_states.clean)
291 {
292 let p1 = repo.dirstate_parents()?.p1;
293 let manifest = repo.manifest_for_node(p1).map_err(|e| {
294 CommandError::from((e, &*format!("{:x}", p1.short())))
295 })?;
296 for to_check in ds_status.unsure {
297 if unsure_is_modified(repo, &manifest, &to_check.path)? {
298 if display_states.modified {
299 ds_status.modified.push(to_check);
300 }
301 } else {
302 if display_states.clean {
303 ds_status.clean.push(to_check.clone());
304 }
305 fixup.push(to_check.path.into_owned())
301 306 }
302 fixup.push(to_check.path.into_owned())
303 307 }
304 308 }
305 }
306 let relative_paths = (!ui.plain(None))
307 && config
308 .get_option(b"commands", b"status.relative")?
309 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
310 let output = DisplayStatusPaths {
311 ui,
312 no_status,
313 relativize: if relative_paths {
314 Some(RelativizePaths::new(repo)?)
315 } else {
316 None
317 },
309 let relative_paths = (!ui.plain(None))
310 && config
311 .get_option(b"commands", b"status.relative")?
312 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
313 let output = DisplayStatusPaths {
314 ui,
315 no_status,
316 relativize: if relative_paths {
317 Some(RelativizePaths::new(repo)?)
318 } else {
319 None
320 },
321 };
322 if display_states.modified {
323 output.display(b"M ", "status.modified", ds_status.modified)?;
324 }
325 if display_states.added {
326 output.display(b"A ", "status.added", ds_status.added)?;
327 }
328 if display_states.removed {
329 output.display(b"R ", "status.removed", ds_status.removed)?;
330 }
331 if display_states.deleted {
332 output.display(b"! ", "status.deleted", ds_status.deleted)?;
333 }
334 if display_states.unknown {
335 output.display(b"? ", "status.unknown", ds_status.unknown)?;
336 }
337 if display_states.ignored {
338 output.display(b"I ", "status.ignored", ds_status.ignored)?;
339 }
340 if display_states.clean {
341 output.display(b"C ", "status.clean", ds_status.clean)?;
342 }
343
344 let dirstate_write_needed = ds_status.dirty;
345 let filesystem_time_at_status_start =
346 ds_status.filesystem_time_at_status_start;
347
348 Ok((
349 fixup,
350 dirstate_write_needed,
351 filesystem_time_at_status_start,
352 ))
318 353 };
319 if display_states.modified {
320 output.display(b"M ", "status.modified", ds_status.modified)?;
321 }
322 if display_states.added {
323 output.display(b"A ", "status.added", ds_status.added)?;
324 }
325 if display_states.removed {
326 output.display(b"R ", "status.removed", ds_status.removed)?;
327 }
328 if display_states.deleted {
329 output.display(b"! ", "status.deleted", ds_status.deleted)?;
330 }
331 if display_states.unknown {
332 output.display(b"? ", "status.unknown", ds_status.unknown)?;
333 }
334 if display_states.ignored {
335 output.display(b"I ", "status.ignored", ds_status.ignored)?;
336 }
337 if display_states.clean {
338 output.display(b"C ", "status.clean", ds_status.clean)?;
339 }
340
341 let mut dirstate_write_needed = ds_status.dirty;
342 let filesystem_time_at_status_start =
343 ds_status.filesystem_time_at_status_start;
354 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
355 dmap.with_status(
356 &AlwaysMatcher,
357 repo.working_directory_path().to_owned(),
358 ignore_files(repo, config),
359 options,
360 after_status,
361 )?;
344 362
345 363 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
346 364 && !dirstate_write_needed
347 365 {
348 366 // Nothing to update
349 367 return Ok(());
350 368 }
351 369
352 370 // Update the dirstate on disk if we can
353 371 let with_lock_result =
354 372 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
355 373 if let Some(mtime_boundary) = filesystem_time_at_status_start {
356 374 for hg_path in fixup {
357 375 use std::os::unix::fs::MetadataExt;
358 376 let fs_path = hg_path_to_path_buf(&hg_path)
359 377 .expect("HgPath conversion");
360 378 // Specifically do not reuse `fs_metadata` from
361 379 // `unsure_is_clean` which was needed before reading
362 380 // contents. Here we access metadata again after reading
363 381 // content, in case it changed in the meantime.
364 382 let fs_metadata = repo
365 383 .working_directory_vfs()
366 384 .symlink_metadata(&fs_path)?;
367 385 if let Some(mtime) =
368 386 TruncatedTimestamp::for_reliable_mtime_of(
369 387 &fs_metadata,
370 388 &mtime_boundary,
371 389 )
372 390 .when_reading_file(&fs_path)?
373 391 {
374 392 let mode = fs_metadata.mode();
375 393 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
376 394 let mut entry = dmap
377 395 .get(&hg_path)?
378 396 .expect("ambiguous file not in dirstate");
379 397 entry.set_clean(mode, size, mtime);
380 398 dmap.add_file(&hg_path, entry)?;
381 399 dirstate_write_needed = true
382 400 }
383 401 }
384 402 }
385 403 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
386 404 if dirstate_write_needed {
387 405 repo.write_dirstate()?
388 406 }
389 407 Ok(())
390 408 });
391 409 match with_lock_result {
392 410 Ok(closure_result) => closure_result?,
393 411 Err(LockError::AlreadyHeld) => {
394 412 // Not updating the dirstate is not ideal but not critical:
395 413 // don’t keep our caller waiting until some other Mercurial
396 414 // process releases the lock.
397 415 }
398 416 Err(LockError::Other(HgError::IoError { error, .. }))
399 417 if error.kind() == io::ErrorKind::PermissionDenied =>
400 418 {
401 419 // `hg status` on a read-only repository is fine
402 420 }
403 421 Err(LockError::Other(error)) => {
404 422 // Report other I/O errors
405 423 Err(error)?
406 424 }
407 425 }
408 426 Ok(())
409 427 }
410 428
411 429 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
412 430 let mut ignore_files = Vec::new();
413 431 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
414 432 if repo_ignore.exists() {
415 433 ignore_files.push(repo_ignore)
416 434 }
417 435 for (key, value) in config.iter_section(b"ui") {
418 436 if key == b"ignore" || key.starts_with(b"ignore.") {
419 437 let path = get_path_from_bytes(value);
420 438 // TODO: expand "~/" and environment variable here, like Python
421 439 // does with `os.path.expanduser` and `os.path.expandvars`
422 440
423 441 let joined = repo.working_directory_path().join(path);
424 442 ignore_files.push(joined);
425 443 }
426 444 }
427 445 ignore_files
428 446 }
429 447
430 448 struct DisplayStatusPaths<'a> {
431 449 ui: &'a Ui,
432 450 no_status: bool,
433 451 relativize: Option<RelativizePaths>,
434 452 }
435 453
436 454 impl DisplayStatusPaths<'_> {
437 455 // Probably more elegant to use a Deref or Borrow trait rather than
438 456 // harcode HgPathBuf, but probably not really useful at this point
439 457 fn display(
440 458 &self,
441 459 status_prefix: &[u8],
442 460 label: &'static str,
443 461 mut paths: Vec<StatusPath<'_>>,
444 462 ) -> Result<(), CommandError> {
445 463 paths.sort_unstable();
446 464 // TODO: get the stdout lock once for the whole loop
447 465 // instead of in each write
448 466 for StatusPath { path, copy_source } in paths {
449 467 let relative;
450 468 let path = if let Some(relativize) = &self.relativize {
451 469 relative = relativize.relativize(&path);
452 470 &*relative
453 471 } else {
454 472 path.as_bytes()
455 473 };
456 474 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
457 475 // in order to stream to stdout instead of allocating an
458 476 // itermediate `Vec<u8>`.
459 477 if !self.no_status {
460 478 self.ui.write_stdout_labelled(status_prefix, label)?
461 479 }
462 480 self.ui
463 481 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
464 482 if let Some(source) = copy_source {
465 483 let label = "status.copied";
466 484 self.ui.write_stdout_labelled(
467 485 &format_bytes!(b" {}\n", source.as_bytes()),
468 486 label,
469 487 )?
470 488 }
471 489 }
472 490 Ok(())
473 491 }
474 492 }
475 493
476 494 /// Check if a file is modified by comparing actual repo store and file system.
477 495 ///
478 496 /// This meant to be used for those that the dirstate cannot resolve, due
479 497 /// to time resolution limits.
480 498 fn unsure_is_modified(
481 499 repo: &Repo,
482 500 manifest: &Manifest,
483 501 hg_path: &HgPath,
484 502 ) -> Result<bool, HgError> {
485 503 let vfs = repo.working_directory_vfs();
486 504 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
487 505 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
488 506 let is_symlink = fs_metadata.file_type().is_symlink();
489 507 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
490 508 // dirstate
491 509 let fs_flags = if is_symlink {
492 510 Some(b'l')
493 511 } else if has_exec_bit(&fs_metadata) {
494 512 Some(b'x')
495 513 } else {
496 514 None
497 515 };
498 516
499 517 let entry = manifest
500 518 .find_by_path(hg_path)?
501 519 .expect("ambgious file not in p1");
502 520 if entry.flags != fs_flags {
503 521 return Ok(true);
504 522 }
505 523 let filelog = repo.filelog(hg_path)?;
506 524 let fs_len = fs_metadata.len();
507 525 let filelog_entry =
508 526 filelog.entry_for_node(entry.node_id()?).map_err(|_| {
509 527 HgError::corrupted("filelog missing node from manifest")
510 528 })?;
511 529 if filelog_entry.file_data_len_not_equal_to(fs_len) {
512 530 // No need to read file contents:
513 531 // it cannot be equal if it has a different length.
514 532 return Ok(true);
515 533 }
516 534
517 535 let p1_filelog_data = filelog_entry.data()?;
518 536 let p1_contents = p1_filelog_data.file_data()?;
519 537 if p1_contents.len() as u64 != fs_len {
520 538 // No need to read file contents:
521 539 // it cannot be equal if it has a different length.
522 540 return Ok(true);
523 541 }
524 542
525 543 let fs_contents = if is_symlink {
526 544 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
527 545 } else {
528 546 vfs.read(fs_path)?
529 547 };
530 548 Ok(p1_contents != &*fs_contents)
531 549 }
General Comments 0
You need to be logged in to leave comments. Login now