robustcheckout.py (30814B)
1 # This software may be used and distributed according to the terms of the 2 # GNU General Public License version 2 or any later version. 3 4 """Robustly perform a checkout. 5 6 This extension provides the ``hg robustcheckout`` command for 7 ensuring a working directory is updated to the specified revision 8 from a source repo using best practices to ensure optimal clone 9 times and storage efficiency. 10 """ 11 12 from __future__ import absolute_import 13 14 import contextlib 15 import json 16 import os 17 import random 18 import re 19 import socket 20 import ssl 21 import time 22 23 from mercurial.i18n import _ 24 from mercurial.node import hex, nullid 25 from mercurial import ( 26 commands, 27 configitems, 28 error, 29 exchange, 30 extensions, 31 hg, 32 match as matchmod, 33 pycompat, 34 registrar, 35 scmutil, 36 urllibcompat, 37 util, 38 vfs, 39 ) 40 41 # Causes worker to purge caches on process exit and for task to retry. 42 EXIT_PURGE_CACHE = 72 43 44 testedwith = ( 45 b"4.5 4.6 4.7 4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 6.0 6.1 6.2 6.3 6.4" 46 ) 47 minimumhgversion = b"4.5" 48 49 cmdtable = {} 50 command = registrar.command(cmdtable) 51 52 configtable = {} 53 configitem = registrar.configitem(configtable) 54 55 configitem(b"robustcheckout", b"retryjittermin", default=configitems.dynamicdefault) 56 configitem(b"robustcheckout", b"retryjittermax", default=configitems.dynamicdefault) 57 58 59 def getsparse(): 60 from mercurial import sparse 61 62 return sparse 63 64 65 def peerlookup(remote, v): 66 with remote.commandexecutor() as e: 67 return e.callcommand(b"lookup", {b"key": v}).result() 68 69 70 @command( 71 b"robustcheckout", 72 [ 73 (b"", b"upstream", b"", b"URL of upstream repo to clone from"), 74 (b"r", b"revision", b"", b"Revision to check out"), 75 (b"b", b"branch", b"", b"Branch to check out"), 76 (b"", b"purge", False, b"Whether to purge the working directory"), 77 (b"", b"sharebase", b"", b"Directory where shared repos should be placed"), 78 ( 79 b"", 80 b"networkattempts", 81 3, 82 b"Maximum number of attempts for network " b"operations", 83 ), 84 (b"", b"sparseprofile", b"", b"Sparse checkout profile to use (path in repo)"), 85 ( 86 b"U", 87 b"noupdate", 88 False, 89 b"the clone will include an empty working directory\n" 90 b"(only a repository)", 91 ), 92 ], 93 b"[OPTION]... URL DEST", 94 norepo=True, 95 ) 96 def robustcheckout( 97 ui, 98 url, 99 dest, 100 upstream=None, 101 revision=None, 102 branch=None, 103 purge=False, 104 sharebase=None, 105 networkattempts=None, 106 sparseprofile=None, 107 noupdate=False, 108 ): 109 """Ensure a working copy has the specified revision checked out. 110 111 Repository data is automatically pooled into the common directory 112 specified by ``--sharebase``, which is a required argument. It is required 113 because pooling storage prevents excessive cloning, which makes operations 114 complete faster. 115 116 One of ``--revision`` or ``--branch`` must be specified. ``--revision`` 117 is preferred, as it is deterministic and there is no ambiguity as to which 118 revision will actually be checked out. 119 120 If ``--upstream`` is used, the repo at that URL is used to perform the 121 initial clone instead of cloning from the repo where the desired revision 122 is located. 123 124 ``--purge`` controls whether to removed untracked and ignored files from 125 the working directory. If used, the end state of the working directory 126 should only contain files explicitly under version control for the requested 127 revision. 128 129 ``--sparseprofile`` can be used to specify a sparse checkout profile to use. 130 The sparse checkout profile corresponds to a file in the revision to be 131 checked out. If a previous sparse profile or config is present, it will be 132 replaced by this sparse profile. We choose not to "widen" the sparse config 133 so operations are as deterministic as possible. If an existing checkout 134 is present and it isn't using a sparse checkout, we error. This is to 135 prevent accidentally enabling sparse on a repository that may have 136 clients that aren't sparse aware. Sparse checkout support requires Mercurial 137 4.3 or newer and the ``sparse`` extension must be enabled. 138 """ 139 if not revision and not branch: 140 raise error.Abort(b"must specify one of --revision or --branch") 141 142 if revision and branch: 143 raise error.Abort(b"cannot specify both --revision and --branch") 144 145 # Require revision to look like a SHA-1. 146 if revision: 147 if ( 148 len(revision) < 12 149 or len(revision) > 40 150 or not re.match(b"^[a-f0-9]+$", revision) 151 ): 152 raise error.Abort( 153 b"--revision must be a SHA-1 fragment 12-40 " b"characters long" 154 ) 155 156 sharebase = sharebase or ui.config(b"share", b"pool") 157 if not sharebase: 158 raise error.Abort( 159 b"share base directory not defined; refusing to operate", 160 hint=b"define share.pool config option or pass --sharebase", 161 ) 162 163 # Sparse profile support was added in Mercurial 4.3, where it was highly 164 # experimental. Because of the fragility of it, we only support sparse 165 # profiles on 4.3. When 4.4 is released, we'll need to opt in to sparse 166 # support. We /could/ silently fall back to non-sparse when not supported. 167 # However, given that sparse has performance implications, we want to fail 168 # fast if we can't satisfy the desired checkout request. 169 if sparseprofile: 170 try: 171 extensions.find(b"sparse") 172 except KeyError: 173 raise error.Abort( 174 b"sparse extension must be enabled to use " b"--sparseprofile" 175 ) 176 177 ui.warn(b"(using Mercurial %s)\n" % util.version()) 178 179 # worker.backgroundclose only makes things faster if running anti-virus, 180 # which our automation doesn't. Disable it. 181 ui.setconfig(b"worker", b"backgroundclose", False) 182 # Don't wait forever if the connection hangs 183 ui.setconfig(b"http", b"timeout", 600) 184 185 # By default the progress bar starts after 3s and updates every 0.1s. We 186 # change this so it shows and updates every 1.0s. 187 # We also tell progress to assume a TTY is present so updates are printed 188 # even if there is no known TTY. 189 # We make the config change here instead of in a config file because 190 # otherwise we're at the whim of whatever configs are used in automation. 191 ui.setconfig(b"progress", b"delay", 1.0) 192 ui.setconfig(b"progress", b"refresh", 1.0) 193 ui.setconfig(b"progress", b"assume-tty", True) 194 195 sharebase = os.path.realpath(sharebase) 196 197 optimes = [] 198 behaviors = set() 199 start = time.time() 200 201 try: 202 return _docheckout( 203 ui, 204 url, 205 dest, 206 upstream, 207 revision, 208 branch, 209 purge, 210 sharebase, 211 optimes, 212 behaviors, 213 networkattempts, 214 sparse_profile=sparseprofile, 215 noupdate=noupdate, 216 ) 217 finally: 218 overall = time.time() - start 219 220 # We store the overall time multiple ways in order to help differentiate 221 # the various "flavors" of operations. 222 223 # ``overall`` is always the total operation time. 224 optimes.append(("overall", overall)) 225 226 def record_op(name): 227 # If special behaviors due to "corrupt" storage occur, we vary the 228 # name to convey that. 229 if "remove-store" in behaviors: 230 name += "_rmstore" 231 if "remove-wdir" in behaviors: 232 name += "_rmwdir" 233 234 optimes.append((name, overall)) 235 236 # We break out overall operations primarily by their network interaction 237 # We have variants within for working directory operations. 238 if "clone" in behaviors and "create-store" in behaviors: 239 record_op("overall_clone") 240 241 if "sparse-update" in behaviors: 242 record_op("overall_clone_sparsecheckout") 243 else: 244 record_op("overall_clone_fullcheckout") 245 246 elif "pull" in behaviors or "clone" in behaviors: 247 record_op("overall_pull") 248 249 if "sparse-update" in behaviors: 250 record_op("overall_pull_sparsecheckout") 251 else: 252 record_op("overall_pull_fullcheckout") 253 254 if "empty-wdir" in behaviors: 255 record_op("overall_pull_emptywdir") 256 else: 257 record_op("overall_pull_populatedwdir") 258 259 else: 260 record_op("overall_nopull") 261 262 if "sparse-update" in behaviors: 263 record_op("overall_nopull_sparsecheckout") 264 else: 265 record_op("overall_nopull_fullcheckout") 266 267 if "empty-wdir" in behaviors: 268 record_op("overall_nopull_emptywdir") 269 else: 270 record_op("overall_nopull_populatedwdir") 271 272 server_url = urllibcompat.urlreq.urlparse(url).netloc 273 274 if "TASKCLUSTER_INSTANCE_TYPE" in os.environ: 275 perfherder = { 276 "framework": { 277 "name": "vcs", 278 }, 279 "suites": [], 280 } 281 for op, duration in optimes: 282 perfherder["suites"].append( 283 { 284 "name": op, 285 "value": duration, 286 "lowerIsBetter": True, 287 "shouldAlert": False, 288 "serverUrl": server_url.decode("utf-8"), 289 "hgVersion": util.version().decode("utf-8"), 290 "extraOptions": [os.environ["TASKCLUSTER_INSTANCE_TYPE"]], 291 "subtests": [], 292 } 293 ) 294 ui.write( 295 b"PERFHERDER_DATA: %s\n" 296 % pycompat.bytestr(json.dumps(perfherder, sort_keys=True)) 297 ) 298 299 300 def _docheckout( 301 ui, 302 url, 303 dest, 304 upstream, 305 revision, 306 branch, 307 purge, 308 sharebase, 309 optimes, 310 behaviors, 311 networkattemptlimit, 312 networkattempts=None, 313 sparse_profile=None, 314 noupdate=False, 315 ): 316 if not networkattempts: 317 networkattempts = [1] 318 319 def callself(): 320 return _docheckout( 321 ui, 322 url, 323 dest, 324 upstream, 325 revision, 326 branch, 327 purge, 328 sharebase, 329 optimes, 330 behaviors, 331 networkattemptlimit, 332 networkattempts=networkattempts, 333 sparse_profile=sparse_profile, 334 noupdate=noupdate, 335 ) 336 337 @contextlib.contextmanager 338 def timeit(op, behavior): 339 behaviors.add(behavior) 340 errored = False 341 try: 342 start = time.time() 343 yield 344 except Exception: 345 errored = True 346 raise 347 finally: 348 elapsed = time.time() - start 349 350 if errored: 351 op += "_errored" 352 353 optimes.append((op, elapsed)) 354 355 ui.write(b"ensuring %s@%s is available at %s\n" % (url, revision or branch, dest)) 356 357 # We assume that we're the only process on the machine touching the 358 # repository paths that we were told to use. This means our recovery 359 # scenario when things aren't "right" is to just nuke things and start 360 # from scratch. This is easier to implement than verifying the state 361 # of the data and attempting recovery. And in some scenarios (such as 362 # potential repo corruption), it is probably faster, since verifying 363 # repos can take a while. 364 365 destvfs = vfs.vfs(dest, audit=False, realpath=True) 366 367 def deletesharedstore(path=None): 368 storepath = path or destvfs.read(b".hg/sharedpath").strip() 369 if storepath.endswith(b".hg"): 370 storepath = os.path.dirname(storepath) 371 372 storevfs = vfs.vfs(storepath, audit=False) 373 storevfs.rmtree(forcibly=True) 374 375 if destvfs.exists() and not destvfs.exists(b".hg"): 376 raise error.Abort(b"destination exists but no .hg directory") 377 378 # Refuse to enable sparse checkouts on existing checkouts. The reasoning 379 # here is that another consumer of this repo may not be sparse aware. If we 380 # enabled sparse, we would lock them out. 381 if destvfs.exists() and sparse_profile and not destvfs.exists(b".hg/sparse"): 382 raise error.Abort( 383 b"cannot enable sparse profile on existing " b"non-sparse checkout", 384 hint=b"use a separate working directory to use sparse", 385 ) 386 387 # And the other direction for symmetry. 388 if not sparse_profile and destvfs.exists(b".hg/sparse"): 389 raise error.Abort( 390 b"cannot use non-sparse checkout on existing sparse " b"checkout", 391 hint=b"use a separate working directory to use sparse", 392 ) 393 394 # Require checkouts to be tied to shared storage because efficiency. 395 if destvfs.exists(b".hg") and not destvfs.exists(b".hg/sharedpath"): 396 ui.warn(b"(destination is not shared; deleting)\n") 397 with timeit("remove_unshared_dest", "remove-wdir"): 398 destvfs.rmtree(forcibly=True) 399 400 # Verify the shared path exists and is using modern pooled storage. 401 if destvfs.exists(b".hg/sharedpath"): 402 storepath = destvfs.read(b".hg/sharedpath").strip() 403 404 ui.write(b"(existing repository shared store: %s)\n" % storepath) 405 406 if not os.path.exists(storepath): 407 ui.warn(b"(shared store does not exist; deleting destination)\n") 408 with timeit("removed_missing_shared_store", "remove-wdir"): 409 destvfs.rmtree(forcibly=True) 410 elif not re.search(rb"[a-f0-9]{40}/\.hg$", storepath.replace(b"\\", b"/")): 411 ui.warn( 412 b"(shared store does not belong to pooled storage; " 413 b"deleting destination to improve efficiency)\n" 414 ) 415 with timeit("remove_unpooled_store", "remove-wdir"): 416 destvfs.rmtree(forcibly=True) 417 418 if destvfs.isfileorlink(b".hg/wlock"): 419 ui.warn( 420 b"(dest has an active working directory lock; assuming it is " 421 b"left over from a previous process and that the destination " 422 b"is corrupt; deleting it just to be sure)\n" 423 ) 424 with timeit("remove_locked_wdir", "remove-wdir"): 425 destvfs.rmtree(forcibly=True) 426 427 def handlerepoerror(e): 428 if pycompat.bytestr(e) == _(b"abandoned transaction found"): 429 ui.warn(b"(abandoned transaction found; trying to recover)\n") 430 repo = hg.repository(ui, dest) 431 if not repo.recover(): 432 ui.warn(b"(could not recover repo state; " b"deleting shared store)\n") 433 with timeit("remove_unrecovered_shared_store", "remove-store"): 434 deletesharedstore() 435 436 ui.warn(b"(attempting checkout from beginning)\n") 437 return callself() 438 439 raise 440 441 # At this point we either have an existing working directory using 442 # shared, pooled storage or we have nothing. 443 444 def handlenetworkfailure(): 445 if networkattempts[0] >= networkattemptlimit: 446 raise error.Abort( 447 b"reached maximum number of network attempts; " b"giving up\n" 448 ) 449 450 ui.warn( 451 b"(retrying after network failure on attempt %d of %d)\n" 452 % (networkattempts[0], networkattemptlimit) 453 ) 454 455 # Do a backoff on retries to mitigate the thundering herd 456 # problem. This is an exponential backoff with a multipler 457 # plus random jitter thrown in for good measure. 458 # With the default settings, backoffs will be: 459 # 1) 2.5 - 6.5 460 # 2) 5.5 - 9.5 461 # 3) 11.5 - 15.5 462 backoff = (2 ** networkattempts[0] - 1) * 1.5 463 jittermin = ui.configint(b"robustcheckout", b"retryjittermin", 1000) 464 jittermax = ui.configint(b"robustcheckout", b"retryjittermax", 5000) 465 backoff += float(random.randint(jittermin, jittermax)) / 1000.0 466 ui.warn(b"(waiting %.2fs before retry)\n" % backoff) 467 time.sleep(backoff) 468 469 networkattempts[0] += 1 470 471 def handlepullerror(e): 472 """Handle an exception raised during a pull. 473 474 Returns True if caller should call ``callself()`` to retry. 475 """ 476 if isinstance(e, error.Abort): 477 if e.args[0] == _(b"repository is unrelated"): 478 ui.warn(b"(repository is unrelated; deleting)\n") 479 destvfs.rmtree(forcibly=True) 480 return True 481 elif e.args[0].startswith(_(b"stream ended unexpectedly")): 482 ui.warn(b"%s\n" % e.args[0]) 483 # Will raise if failure limit reached. 484 handlenetworkfailure() 485 return True 486 # TODO test this branch 487 elif isinstance(e, error.ResponseError): 488 if e.args[0].startswith(_(b"unexpected response from remote server:")): 489 ui.warn(b"(unexpected response from remote server; retrying)\n") 490 destvfs.rmtree(forcibly=True) 491 # Will raise if failure limit reached. 492 handlenetworkfailure() 493 return True 494 elif isinstance(e, ssl.SSLError): 495 # Assume all SSL errors are due to the network, as Mercurial 496 # should convert non-transport errors like cert validation failures 497 # to error.Abort. 498 ui.warn(b"ssl error: %s\n" % pycompat.bytestr(str(e))) 499 handlenetworkfailure() 500 return True 501 elif isinstance(e, urllibcompat.urlerr.httperror) and e.code >= 500: 502 ui.warn(b"http error: %s\n" % pycompat.bytestr(str(e.reason))) 503 handlenetworkfailure() 504 return True 505 elif isinstance(e, urllibcompat.urlerr.urlerror): 506 if isinstance(e.reason, socket.error): 507 ui.warn(b"socket error: %s\n" % pycompat.bytestr(str(e.reason))) 508 handlenetworkfailure() 509 return True 510 else: 511 ui.warn( 512 b"unhandled URLError; reason type: %s; value: %s\n" 513 % ( 514 pycompat.bytestr(e.reason.__class__.__name__), 515 pycompat.bytestr(str(e.reason)), 516 ) 517 ) 518 elif isinstance(e, socket.timeout): 519 ui.warn(b"socket timeout\n") 520 handlenetworkfailure() 521 return True 522 else: 523 ui.warn( 524 b"unhandled exception during network operation; type: %s; " 525 b"value: %s\n" 526 % (pycompat.bytestr(e.__class__.__name__), pycompat.bytestr(str(e))) 527 ) 528 529 return False 530 531 # Perform sanity checking of store. We may or may not know the path to the 532 # local store. It depends if we have an existing destvfs pointing to a 533 # share. To ensure we always find a local store, perform the same logic 534 # that Mercurial's pooled storage does to resolve the local store path. 535 cloneurl = upstream or url 536 537 try: 538 clonepeer = hg.peer(ui, {}, cloneurl) 539 rootnode = peerlookup(clonepeer, b"0") 540 except error.RepoLookupError: 541 raise error.Abort(b"unable to resolve root revision from clone " b"source") 542 except ( 543 error.Abort, 544 ssl.SSLError, 545 urllibcompat.urlerr.urlerror, 546 socket.timeout, 547 ) as e: 548 if handlepullerror(e): 549 return callself() 550 raise 551 552 if rootnode == nullid: 553 raise error.Abort(b"source repo appears to be empty") 554 555 storepath = os.path.join(sharebase, hex(rootnode)) 556 storevfs = vfs.vfs(storepath, audit=False) 557 558 if storevfs.isfileorlink(b".hg/store/lock"): 559 ui.warn( 560 b"(shared store has an active lock; assuming it is left " 561 b"over from a previous process and that the store is " 562 b"corrupt; deleting store and destination just to be " 563 b"sure)\n" 564 ) 565 if destvfs.exists(): 566 with timeit("remove_dest_active_lock", "remove-wdir"): 567 destvfs.rmtree(forcibly=True) 568 569 with timeit("remove_shared_store_active_lock", "remove-store"): 570 storevfs.rmtree(forcibly=True) 571 572 if storevfs.exists() and not storevfs.exists(b".hg/requires"): 573 ui.warn( 574 b"(shared store missing requires file; this is a really " 575 b"odd failure; deleting store and destination)\n" 576 ) 577 if destvfs.exists(): 578 with timeit("remove_dest_no_requires", "remove-wdir"): 579 destvfs.rmtree(forcibly=True) 580 581 with timeit("remove_shared_store_no_requires", "remove-store"): 582 storevfs.rmtree(forcibly=True) 583 584 if storevfs.exists(b".hg/requires"): 585 requires = set(storevfs.read(b".hg/requires").splitlines()) 586 # "share-safe" (enabled by default as of hg 6.1) moved most 587 # requirements to a new file, so we need to look there as well to avoid 588 # deleting and re-cloning each time 589 if b"share-safe" in requires: 590 requires |= set(storevfs.read(b".hg/store/requires").splitlines()) 591 # FUTURE when we require generaldelta, this is where we can check 592 # for that. 593 required = {b"dotencode", b"fncache"} 594 595 missing = required - requires 596 if missing: 597 ui.warn( 598 b"(shared store missing requirements: %s; deleting " 599 b"store and destination to ensure optimal behavior)\n" 600 % b", ".join(sorted(missing)) 601 ) 602 if destvfs.exists(): 603 with timeit("remove_dest_missing_requires", "remove-wdir"): 604 destvfs.rmtree(forcibly=True) 605 606 with timeit("remove_shared_store_missing_requires", "remove-store"): 607 storevfs.rmtree(forcibly=True) 608 609 created = False 610 611 if not destvfs.exists(): 612 # Ensure parent directories of destination exist. 613 # Mercurial 3.8 removed ensuredirs and made makedirs race safe. 614 if util.safehasattr(util, "ensuredirs"): 615 makedirs = util.ensuredirs 616 else: 617 makedirs = util.makedirs 618 619 makedirs(os.path.dirname(destvfs.base), notindexed=True) 620 makedirs(sharebase, notindexed=True) 621 622 if upstream: 623 ui.write(b"(cloning from upstream repo %s)\n" % upstream) 624 625 if not storevfs.exists(): 626 behaviors.add(b"create-store") 627 628 try: 629 with timeit("clone", "clone"): 630 shareopts = {b"pool": sharebase, b"mode": b"identity"} 631 res = hg.clone( 632 ui, 633 {}, 634 clonepeer, 635 dest=dest, 636 update=False, 637 shareopts=shareopts, 638 stream=True, 639 ) 640 except ( 641 error.Abort, 642 ssl.SSLError, 643 urllibcompat.urlerr.urlerror, 644 socket.timeout, 645 ) as e: 646 if handlepullerror(e): 647 return callself() 648 raise 649 except error.RepoError as e: 650 return handlerepoerror(e) 651 except error.RevlogError as e: 652 ui.warn(b"(repo corruption: %s; deleting shared store)\n" % e) 653 with timeit("remove_shared_store_revlogerror", "remote-store"): 654 deletesharedstore() 655 return callself() 656 657 # TODO retry here. 658 if res is None: 659 raise error.Abort(b"clone failed") 660 661 # Verify it is using shared pool storage. 662 if not destvfs.exists(b".hg/sharedpath"): 663 raise error.Abort(b"clone did not create a shared repo") 664 665 created = True 666 667 # The destination .hg directory should exist. Now make sure we have the 668 # wanted revision. 669 670 repo = hg.repository(ui, dest) 671 672 # We only pull if we are using symbolic names or the requested revision 673 # doesn't exist. 674 havewantedrev = False 675 676 if revision: 677 try: 678 ctx = scmutil.revsingle(repo, revision) 679 except error.RepoLookupError: 680 ctx = None 681 682 if ctx: 683 if not ctx.hex().startswith(revision): 684 raise error.Abort( 685 b"--revision argument is ambiguous", 686 hint=b"must be the first 12+ characters of a " b"SHA-1 fragment", 687 ) 688 689 checkoutrevision = ctx.hex() 690 havewantedrev = True 691 692 if not havewantedrev: 693 ui.write(b"(pulling to obtain %s)\n" % (revision or branch,)) 694 695 remote = None 696 try: 697 remote = hg.peer(repo, {}, url) 698 pullrevs = [peerlookup(remote, revision or branch)] 699 checkoutrevision = hex(pullrevs[0]) 700 if branch: 701 ui.warn( 702 b"(remote resolved %s to %s; " 703 b"result is not deterministic)\n" % (branch, checkoutrevision) 704 ) 705 706 if checkoutrevision in repo: 707 ui.warn(b"(revision already present locally; not pulling)\n") 708 else: 709 with timeit("pull", "pull"): 710 pullop = exchange.pull(repo, remote, heads=pullrevs) 711 if not pullop.rheads: 712 raise error.Abort(b"unable to pull requested revision") 713 except ( 714 error.Abort, 715 ssl.SSLError, 716 urllibcompat.urlerr.urlerror, 717 socket.timeout, 718 ) as e: 719 if handlepullerror(e): 720 return callself() 721 raise 722 except error.RepoError as e: 723 return handlerepoerror(e) 724 except error.RevlogError as e: 725 ui.warn(b"(repo corruption: %s; deleting shared store)\n" % e) 726 deletesharedstore() 727 return callself() 728 finally: 729 if remote: 730 remote.close() 731 732 # Now we should have the wanted revision in the store. Perform 733 # working directory manipulation. 734 735 # Avoid any working directory manipulations if `-U`/`--noupdate` was passed 736 if noupdate: 737 ui.write(b"(skipping update since `-U` was passed)\n") 738 return None 739 740 # Purge if requested. We purge before update because this way we're 741 # guaranteed to not have conflicts on `hg update`. 742 if purge and not created: 743 ui.write(b"(purging working directory)\n") 744 purge = getattr(commands, "purge", None) 745 if not purge: 746 purge = extensions.find(b"purge").purge 747 748 # Mercurial 4.3 doesn't purge files outside the sparse checkout. 749 # See https://bz.mercurial-scm.org/show_bug.cgi?id=5626. Force 750 # purging by monkeypatching the sparse matcher. 751 try: 752 old_sparse_fn = getattr(repo.dirstate, "_sparsematchfn", None) 753 if old_sparse_fn is not None: 754 repo.dirstate._sparsematchfn = lambda: matchmod.always() 755 756 with timeit("purge", "purge"): 757 if purge( 758 ui, 759 repo, 760 all=True, 761 abort_on_err=True, 762 # The function expects all arguments to be 763 # defined. 764 **{"print": None, "print0": None, "dirs": None, "files": None} 765 ): 766 raise error.Abort(b"error purging") 767 finally: 768 if old_sparse_fn is not None: 769 repo.dirstate._sparsematchfn = old_sparse_fn 770 771 # Update the working directory. 772 773 if repo[b"."].node() == nullid: 774 behaviors.add("empty-wdir") 775 else: 776 behaviors.add("populated-wdir") 777 778 if sparse_profile: 779 sparsemod = getsparse() 780 781 # By default, Mercurial will ignore unknown sparse profiles. This could 782 # lead to a full checkout. Be more strict. 783 try: 784 repo.filectx(sparse_profile, changeid=checkoutrevision).data() 785 except error.ManifestLookupError: 786 raise error.Abort( 787 b"sparse profile %s does not exist at revision " 788 b"%s" % (sparse_profile, checkoutrevision) 789 ) 790 791 old_config = sparsemod.parseconfig( 792 repo.ui, repo.vfs.tryread(b"sparse"), b"sparse" 793 ) 794 795 old_includes, old_excludes, old_profiles = old_config 796 797 if old_profiles == {sparse_profile} and not old_includes and not old_excludes: 798 ui.write( 799 b"(sparse profile %s already set; no need to update " 800 b"sparse config)\n" % sparse_profile 801 ) 802 else: 803 if old_includes or old_excludes or old_profiles: 804 ui.write( 805 b"(replacing existing sparse config with profile " 806 b"%s)\n" % sparse_profile 807 ) 808 else: 809 ui.write(b"(setting sparse config to profile %s)\n" % sparse_profile) 810 811 # If doing an incremental update, this will perform two updates: 812 # one to change the sparse profile and another to update to the new 813 # revision. This is not desired. But there's not a good API in 814 # Mercurial to do this as one operation. 815 # TRACKING hg64 - Mercurial 6.4 and later require call to 816 # dirstate.changing_parents(repo) 817 def parentchange(repo): 818 if util.safehasattr(repo.dirstate, "changing_parents"): 819 return repo.dirstate.changing_parents(repo) 820 return repo.dirstate.parentchange() 821 822 with repo.wlock(), parentchange(repo), timeit( 823 "sparse_update_config", "sparse-update-config" 824 ): 825 # pylint --py3k: W1636 826 fcounts = list( 827 map( 828 len, 829 sparsemod._updateconfigandrefreshwdir( 830 repo, [], [], [sparse_profile], force=True 831 ), 832 ) 833 ) 834 835 repo.ui.status( 836 b"%d files added, %d files dropped, " 837 b"%d files conflicting\n" % tuple(fcounts) 838 ) 839 840 ui.write(b"(sparse refresh complete)\n") 841 842 op = "update_sparse" if sparse_profile else "update" 843 behavior = "update-sparse" if sparse_profile else "update" 844 845 with timeit(op, behavior): 846 if commands.update(ui, repo, rev=checkoutrevision, clean=True): 847 raise error.Abort(b"error updating") 848 849 ui.write(b"updated to %s\n" % checkoutrevision) 850 851 return None 852 853 854 def extsetup(ui): 855 # Ensure required extensions are loaded. 856 for ext in (b"purge", b"share"): 857 try: 858 extensions.find(ext) 859 except KeyError: 860 extensions.load(ui, ext, None)