Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 78 additions & 6 deletions src/taskgraph/run-task/robustcheckout.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import re
import socket
import ssl
import stat
import time

from mercurial.i18n import _
Expand All @@ -38,6 +39,22 @@
vfs,
)

# TRACKING hg72 - `hg.repository`/`hg.peer` moved to `mercurial.repo.factory`.
try:
from mercurial.repo import factory as _repofactory

_repository = _repofactory.repository
_peer = _repofactory.peer
except ImportError:
_repository = hg.repository
_peer = hg.peer

# TRACKING hg72 - `hg.clone` moved to `mercurial.cmd_impls.clone.clone`.
try:
from mercurial.cmd_impls.clone import clone as _clone
except ImportError:
_clone = hg.clone

# Causes worker to purge caches on process exit and for task to retry.
EXIT_PURGE_CACHE = 72

Expand Down Expand Up @@ -67,6 +84,57 @@ def peerlookup(remote, v):
return e.callcommand(b"lookup", {b"key": v}).result()


def remove_dangling_links(ui, path):
"""On Windows, remove dangling symlinks and junctions under ``path``.

npm's ``file:`` protocol dependencies create directory junctions in
``node_modules/`` with absolute targets. When a Taskcluster cache
is restored to a different task directory those targets no longer
exist, and Mercurial's purge crashes in ``vfs.listdir()`` with
``FileNotFoundError``. Removing them first lets purge proceed.
"""
if os.name != "nt":
return

ui.write(b"windows detected, removing dangling links\n")

stack = [path]
while stack:
dirpath = stack.pop()
try:
entries = os.scandir(dirpath)
except OSError:
continue

with entries:
for entry in entries:
p = entry.path
try:
attrs = entry.stat(follow_symlinks=False).st_file_attributes
except OSError:
continue

if attrs & stat.FILE_ATTRIBUTE_REPARSE_POINT:
if not os.path.exists(p):
ui.write(
b"(removing dangling link %s)\n"
% os.fsencode(os.path.relpath(p, path))
)
try:
os.rmdir(p)
except OSError:
os.remove(p)
continue

try:
is_dir = entry.is_dir(follow_symlinks=False)
except OSError:
continue

if is_dir:
stack.append(p)


@command(
b"robustcheckout",
[
Expand Down Expand Up @@ -427,7 +495,7 @@ def deletesharedstore(path=None):
def handlerepoerror(e):
if pycompat.bytestr(e) == _(b"abandoned transaction found"):
ui.warn(b"(abandoned transaction found; trying to recover)\n")
repo = hg.repository(ui, dest)
repo = _repository(ui, dest)
if not repo.recover():
ui.warn(b"(could not recover repo state; deleting shared store)\n")
with timeit("remove_unrecovered_shared_store", "remove-store"):
Expand Down Expand Up @@ -535,7 +603,7 @@ def handlepullerror(e):
cloneurl = upstream or url

try:
clonepeer = hg.peer(ui, {}, cloneurl)
clonepeer = _peer(ui, {}, cloneurl)
rootnode = peerlookup(clonepeer, b"0")
except error.RepoLookupError:
raise error.Abort(b"unable to resolve root revision from clone source")
Expand Down Expand Up @@ -623,12 +691,12 @@ def handlepullerror(e):
ui.write(b"(cloning from upstream repo %s)\n" % upstream)

if not storevfs.exists():
behaviors.add(b"create-store")
behaviors.add("create-store")

try:
with timeit("clone", "clone"):
shareopts = {b"pool": sharebase, b"mode": b"identity"}
res = hg.clone(
res = _clone(
ui,
{},
clonepeer,
Expand Down Expand Up @@ -667,7 +735,7 @@ def handlepullerror(e):
# The destination .hg directory should exist. Now make sure we have the
# wanted revision.

repo = hg.repository(ui, dest)
repo = _repository(ui, dest)

# We only pull if we are using symbolic names or the requested revision
# doesn't exist.
Expand All @@ -694,7 +762,7 @@ def handlepullerror(e):

remote = None
try:
remote = hg.peer(repo, {}, url)
remote = _peer(repo, {}, url)
pullrevs = [peerlookup(remote, revision or branch)]
checkoutrevision = hex(pullrevs[0])
if branch:
Expand Down Expand Up @@ -741,6 +809,10 @@ def handlepullerror(e):
# guaranteed to not have conflicts on `hg update`.
if purge and not created:
ui.write(b"(purging working directory)\n")

with timeit("purge", "dangling_link_remove"):
remove_dangling_links(ui, dest)

purge = getattr(commands, "purge", None)
if not purge:
purge = extensions.find(b"purge").purge
Expand Down