Skip to content
97 changes: 87 additions & 10 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
`xf-project-<id>` topic on all three (skipped for local remotes),
and then VERIFIES by blob sha that every source path landed in
exactly one place, and that the root's only submodules are the
two leg mounts.
two leg mounts, each at the commit pushed to its leg.

THE PLAN IS AN ARTIFACT A HUMAN OR AN AI EDITS. That is why it is YAML with
reasons in it rather than a pipe between two processes: the classifier is
Expand Down Expand Up @@ -3287,11 +3287,16 @@ def _mount_the_legs(assembly: Path, work_root: Path, names: dict, urls: dict,
# `--force` also lets through a path the index holds with a gitlink,
# reusing the repository there, and skips git's check for a path
# beyond a symlink: `_refuse_an_unmountable_path` refuses both first.
# With that, the two paths are distinct, so the second mount's name
# has no `.git/modules/<name>` in this fresh clone for `--force` to
# reactivate; and a newer git's renaming of a name already in use
# can only meet a `.gitmodules` the plan KEPT here, which
# verification never passes (#165).
# With that, the two paths are distinct, and the assembly was cloned
# with NO TEMPLATE (`_commit_the_split`, #243), so `.git/modules`
# holds nothing before the first mount and only that mount's store
# at the second: no `.git/modules/<name>` is there for `--force` to
# reactivate. (An operator's template could plant one, and the mount
# then took the stranger's commit.) Whatever else put a different
# commit at a mount, `_verify` compares each gitlink with the commit
# pushed to its leg (`adopt-mount-commit`). A newer git's renaming of
# a name already in use can only meet a `.gitmodules` the plan KEPT
# here, which verification never passes (#165).
_refuse_an_unmountable_path(assembly, path)
run(["git", *FILE_PROTOCOL, "submodule", "add", "--force", "-q",
str(work_root / names[role]), path], cwd=assembly)
Expand Down Expand Up @@ -3646,7 +3651,28 @@ def _commit_the_split(plan: Plan, source: Source, assembly: Path,
also the pull request's body — or None when the push was REFUSED, having
already said so. Split out of `cmd_execute` for #138.
"""
run(["git", *FILE_PROTOCOL, "clone", "-q", str(source.path), str(assembly)])
# NO TEMPLATE (#243), as the scaffold's `git init` takes none (#229).
# `git clone` copies the operator's template (`init.templateDir`, or
# `GIT_TEMPLATE_DIR`) into `.git`, and one that holds a `modules/<leg
# path>` store, as a copy of a working Triad root's `.git` does, plants
# `.git/modules/<leg path>`. `git submodule add --force` in
# `_mount_the_legs` REACTIVATES a store that is already there where it
# would refuse without `--force`, so each mount took the STRANGER's
# store: exit 0, a pushed split whose gitlinks were that store's commit
# and not the legs' tips, `adoption verified` over it, and a recursive
# clone that exits 128. The empty `--template=` is git's documented way
# to use none. The assembly is a build directory this tool fills and
# pushes, so no template content belongs in it, and whoever clones the
# pushed root still gets their template from their own `git clone`. Nothing
# else `execute` makes needs it: the legs' bare remotes and work trees are
# never mounted into, and the mounts only READ the work trees, a clone
# copying their objects and refs and never a `.git/modules`. With no
# template the assembly has no `.git/hooks`, so HOOKS a template installs
# no longer run on the split's commit and push, as they did before #243;
# hooks set with `core.hooksPath` still do, and the legs keep their
# template's hooks.
run(["git", *FILE_PROTOCOL, "clone", "-q", "--template=",
str(source.path), str(assembly)])
run(["git", "checkout", "-q", "-B", branch, source.commit], cwd=assembly)
_mount_the_legs(assembly, work_root, names, urls, paths_for, spec_path,
code_path)
Expand Down Expand Up @@ -3875,6 +3901,16 @@ def _verify(source: Source, assembly: Path, work_root: Path, names,
because the root keeps no gitlink but the two mounts: this check fails
closed on one, and does not lean on `check`'s refusals to keep it out.
`_land_the_root_tree` says which.

EACH MOUNT IS THE COMMIT THAT WAS PUSHED TO ITS LEG (#243). The legs'
trees account for the source's blobs whichever commit the root mounts, so
a mount at ANOTHER commit passed every check above: a module store `git
submodule add --force` reactivated (an operator's template planted one)
made a split whose gitlinks were a stranger's commit, which was pushed
and said `adoption verified`. `_mount_commit_findings` compares each
gitlink with the `HEAD` of its leg's work directory: the commit pushed,
and the tree read above. It does not undo the push, as no finding does;
the clone's `--template=` is what keeps a planted store out.
"""
print("\nVERIFICATION — every source path at "
f"{source.commit[:12]}, by blob sha")
Expand All @@ -3883,9 +3919,11 @@ def _verify(source: Source, assembly: Path, work_root: Path, names,
for role in ("spec", "code"):
for path, _, oid, _ in _tree_of(work_root / names[role], "HEAD"):
after.setdefault(path, []).append(f"{role}:{oid}")
root_findings = _land_the_root_tree(
after, _tree_of(assembly, split_commit), before,
dict(zip((spec_path, code_path), EXTRACTED_LEGS)))
mounts = dict(zip((spec_path, code_path), EXTRACTED_LEGS))
split_tree = _tree_of(assembly, split_commit)
root_findings = _land_the_root_tree(after, split_tree, before, mounts)
root_findings += _mount_commit_findings(
split_tree, mounts, _pushed_commits(work_root, names))

counts, findings = _account_for(before, after, paths_for["drop"])
findings += _shape_file_findings(assembly, split_commit)
Expand Down Expand Up @@ -3955,6 +3993,12 @@ def _account_for(before: dict, after: dict, drops: list[str]) -> tuple[dict, lis
"this is not how the plan spells its leg paths: `execute` refuses "
"before any leg exists a leg path git would record at another spelling "
f"(`plan-bad-leg-path`), {SPLIT_IS_WRONG}")
#: The exit of `adopt-mount-commit`: a plan never says which commit a mount is.
MOUNT_COMMIT_DEFECT = (
"no plan chooses a mount's commit: each leg is mounted from its own work "
"directory, and a module store already under `.git/modules/<name>`, which "
"`git submodule add --force` reactivates, is the known way another "
f"repository's commit is mounted instead, {SPLIT_IS_WRONG}")
#: The exit of `adopt-root-submodule`.
ROOT_SUBMODULE_DEFECT = (
"`execute` refuses before any leg exists a plan that keeps a submodule "
Expand Down Expand Up @@ -4054,6 +4098,39 @@ def _what_is_at(tree: list, path: str) -> str:
return "nothing"


def _mount_commit_findings(tree: list, mounts: dict[str, str],
pushed: dict[str, str]) -> list[str]:
"""`adopt-mount-commit` for each mount whose gitlink names any commit but
the one pushed to its leg (#243); `mounts` is `_land_the_root_tree`'s and
`pushed` is `_pushed_commits`'.

The pin the split wrote names the pushed commit, and a mount at another
leaves a recursive clone unable to satisfy both: it fails (exit 128), or
checks out a tree nobody pushed there. The question is ASKED OF A GITLINK
ONLY: a mount path that holds none is `adopt-mount-missing`
(`_land_the_root_tree`), and said once. It is asked here, beside that
function, because that one lands paths and knows nothing of what was
pushed.
"""
return [f"FINDING adopt-mount-commit: {path} is a gitlink to {oid[:12]} "
f"in the root tree, where the {mounts[path]} leg was pushed at "
f"{pushed[mounts[path]][:12]}, so the mount is not the leg this "
"run pushed and a recursive clone of the split checks out the "
f"wrong commit or cannot fetch one; {MOUNT_COMMIT_DEFECT}"
for path, mode, oid, _ in tree
if mode == GITLINK_MODE and path in mounts
and oid != pushed[mounts[path]]]


def _pushed_commits(work_root: Path, names: dict) -> dict[str, str]:
"""The commit `execute` pushed to each leg: the `HEAD` of its work
directory, which `_extract_leg` and `_seed_leg` push as
`HEAD:refs/heads/<tracking>` and whose tree `_verify` accounts for."""
return {role: git_out(["rev-parse", "HEAD"],
cwd=work_root / names[role]).lower()
for role in EXTRACTED_LEGS}


def _tree_of(repo: Path, rev: str) -> list[tuple[str, str, str, int]]:
raw = git_out(["ls-tree", "-r", "-z", rev], cwd=repo, binary=True)
out = []
Expand Down
Loading
Loading