Skip to content
Merged
Show file tree
Hide file tree
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
64 changes: 48 additions & 16 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -9138,9 +9138,11 @@ def _root_attributes(source: Source, walks: list, disk: _KeptTree,
"""The `.gitattributes` the split puts at the root, and under `shape/`,
as `{path: bytes}`. Where the root keeps none, the shape's own. Where it
keeps one, the shape's goes beside it and the split appends the block
for the copies `names` outside `shape/` (#471); a kept one that is not a
regular file gets no block, so it is asked as the block alone, the lines
#471's follow-up asks a regular file in its place to hold."""
for the copies `names` outside `shape/` (#471), each named where `walks`
land it, the path git tracks, and by its own name where that differs
(#807); a kept one that is not a regular file gets no block, so it is
asked as the block alone, the lines #471's follow-up asks a regular file
in its place to hold."""
shapes = (SPLIT_TEMPLATE / ATTRIBUTES).read_text(
encoding="utf-8").encode("utf-8")
walk = next((walk for walk in walks if walk.path == ATTRIBUTES), None)
Expand All @@ -9150,8 +9152,8 @@ def _root_attributes(source: Source, walks: list, disk: _KeptTree,
blobs = {path: oid for path, _, oid, _ in tree}
own = (_blob_bytes(source.path, blobs[row[0]])
if row and row[1] in FILE_MODES else b"")
return {ATTRIBUTES: with_eol_block(own, eol_block_paths(names,
COLLISION_DIR)),
return {ATTRIBUTES: with_eol_block(own, eol_block_paths(
names, COLLISION_DIR, dict(_pinned_landings(walks)))),
f"{COLLISION_DIR}/{ATTRIBUTES}": shapes}


Expand Down Expand Up @@ -9730,12 +9732,14 @@ def _rules_reaching(asked: _SplitRehearsal, source: Source, tree: list,

def _as_the_split_writes(asked: _SplitRehearsal, rules: list, kept: set[str],
own: str | None, written: dict,
names: list[str]) -> None:
landings: list[tuple[str, str]]) -> None:
"""The root's rules as the split's `git add` reads them: the moved
`.gitattributes` gone; the root's own followed by #471's block, or the
shape's own where the root keeps none; and the shape's beside it, where
the root keeps nothing there (`plan-root-blocks-shape-copy` names what
it keeps, #262)."""
it keeps, #262). The block names each pinned copy of `landings`
(`_pinned_landings`') where it lands, the path git tracks, and by its own
name where that differs (#807)."""
for path, _, _ in rules:
if path not in kept:
asked.work.joinpath(*path.split("/")).unlink(missing_ok=True)
Expand All @@ -9746,7 +9750,8 @@ def _as_the_split_writes(asked: _SplitRehearsal, rules: list, kept: set[str],
if own is not None:
asked.place(ATTRIBUTES, with_eol_block(
(asked.work / ATTRIBUTES).read_bytes(),
eol_block_paths(names, COLLISION_DIR)))
eol_block_paths([name for name, _ in landings], COLLISION_DIR,
dict(landings))))
if beside not in kept:
asked.place(beside, written[beside])

Expand Down Expand Up @@ -9926,7 +9931,7 @@ def _reencoded_files_findings(plan: Plan, source: Source, tree: list,
["diff-files", "--name-only", "-z"], TAKEN) if path in moved]
# The split's side: its `git add`, over the root it writes.
try:
_as_the_split_writes(asked, rules, kept, own, written, names)
_as_the_split_writes(asked, rules, kept, own, written, landings)
except OSError:
return [_moved_finding(taken)] if taken else []
held = {*[row for row in wanted if row[0] in kept],
Expand Down Expand Up @@ -11989,10 +11994,9 @@ def _commit_the_split(plan: Plan, source: Source, assembly: Path,
print(f" beside {actual} (the source already has {intended}; "
"nothing was overwritten)")
if materialized.eol_block:
print(f" appended to {ATTRIBUTES}: a `text eol=lf` line for "
f"each of {len(materialized.eol_block)} pinned copies, so "
"every checkout keeps their bytes (#471); none of its own "
"lines was changed")
print(f" appended to {ATTRIBUTES}: "
f"{_appended_lines(materialized)}, so every checkout keeps "
"their bytes (#471); none of its own lines was changed")

follow_ups = _plan_follow_ups(plan)
message = _split_message(names, paths_for, leg_commits, spec_path,
Expand Down Expand Up @@ -12582,6 +12586,20 @@ def _accounted_for(source: Source, assembly: Path, work_root: Path, names,
return findings + root_findings


def _appended_lines(materialized: Materialized) -> str:
"""What `execute` says it appended to the source's `.gitattributes`: a
line for each pinned copy outside `shape/`, and the second line each
copy written through a symlink has under its pinned path (#807)."""
copies = len(eol_block_paths(materialized.shape_files, COLLISION_DIR))
said = f"a `text eol=lf` line for each of {copies} pinned copies"
second = len(materialized.eol_block) - copies
if second:
said += (f", and {second} more under the pinned path of each copy "
"written through a symlink, which governs it once the link "
"is replaced")
return said


def _land_the_eol_block(assembly: Path, split_commit: str, tree: list,
before: dict, after: dict) -> int:
"""Count the source's `.gitattributes` as landed in the root when the
Expand All @@ -12606,7 +12624,10 @@ def _land_the_eol_block(assembly: Path, split_commit: str, tree: list,
a lookup among the tree's plain files found none, so the appended file
was `adopt-lost` where main verified it (#471's review, SHOULD-3). A pin
neither can read counts nothing here, and `_shape_file_findings` reports
it.
it. Each copy the block names is named where the split's symlinks lead
its pinned path (`_SplitTree.tracked_at`), the path git tracks, and then
by that pinned path where it differs, as the materializer named it
(#807).
"""
source_oid = before.get(ATTRIBUTES)
landed = after.get(ATTRIBUTES, [])
Expand All @@ -12615,11 +12636,13 @@ def _land_the_eol_block(assembly: Path, split_commit: str, tree: list,
or root[0] == f"root:{source_oid}":
return 0
blobs = {path: oid for path, mode, oid, _ in tree if mode in FILE_MODES}
pin = _pin_through_links(_SplitTree(assembly, split_commit))
split = _SplitTree(assembly, split_commit)
pin = _pin_through_links(split)
pinned = _pinned_shape_paths(assembly, pin) if pin else None
if pinned is None or blobs.get(ATTRIBUTES) != root[0].split(":", 1)[1]:
return 0
paths = eol_block_paths(pinned, COLLISION_DIR)
paths = eol_block_paths(pinned, COLLISION_DIR,
{path: split.tracked_at(path) for path in pinned})
expected = with_eol_block(_blob_bytes(assembly, source_oid), paths)
if _blob_bytes(assembly, blobs[ATTRIBUTES]) != expected:
return 0
Expand Down Expand Up @@ -13031,6 +13054,15 @@ def blob_at(self, path: str) -> str:
f"is not in the split commit {self.commit}")
return oid

def tracked_at(self, path: str) -> str | None:
"""Where the split tracks what `path` leads to, each symlink on the
way followed (#807); None where the way leads out of the tree, above
it, round a loop, or through what is no directory in it."""
try:
return self._resolve(path)
except _NotInTheSplitError:
return None

def _resolve(self, path: str) -> str:
"""`path` with each symlink on the way followed, as `open()` does."""
parts, done, via, hops = self._parts(path, ""), [], "", 0
Expand Down
66 changes: 57 additions & 9 deletions scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,25 @@ def readme_trailing_shape_line(text: str) -> "re.Match | None":
# block's. `adopt-project.py`'s verification counts the file as the source's
# bytes followed by exactly this block, and `update-shape.py apply --add`
# extends it when the standard adds a copy at the root later.
#
# EACH COPY IS NAMED WHERE GIT TRACKS IT (#807). A source may keep a
# symlinked directory in the root that the shape writes through (`.github ->
# ci`, a layout #504 accepts), and git tracks a copy written through it where
# the link leads: `ci/workflows/validate.yml`. A line naming only the link's
# spelling, `/.github/workflows/validate.yml`, governed nothing, so under the
# source's `* text=auto eol=crlf` a clone wrote that copy with CRLF, and
# `validate-pins.py` reported shape-copy-drift after `execute` had said
# `adoption verified`. So each copy is named where the symlinks inside the
# root lead it, a chain of them followed to its end, as `open()` followed
# them when the copy was written, and then, where that differs, by its pinned
# path on the next line. That second line governs nothing while the link is
# kept. It governs the copy once a person replaces the link with what it
# points at, which `update-shape.py` asks for before it rewrites a copy
# reached through one: without it that clone wrote the copy with CRLF, where
# main's had healed (#844's review). Where the links lead out of the root or
# cannot be followed, or to a name no line can spell literally, the copy has
# its pinned line alone, as before. The pin still names each copy by the path
# it was written at, which a clone opens through the same link.

#: The `.gitattributes` a source may already hold at its root.
ATTRIBUTES = ".gitattributes"
Expand Down Expand Up @@ -734,12 +753,26 @@ def eol_line(path: str) -> str:
return f"/{path} text eol=lf"


def eol_block_paths(shape_files, collision_dir: str) -> list[str]:
"""The pinned copies a source's own root `.gitattributes` governs: each
one not under `collision_dir`, where the shape's own copy of the file
governs, in the pin's order."""
return [path for path in shape_files
if not path.startswith(f"{collision_dir}/")]
def eol_block_paths(shape_files, collision_dir: str,
tracked: dict | None = None) -> list[str]:
"""The paths the block names for the pinned copies a source's own root
`.gitattributes` governs: each one not under `collision_dir`, where the
shape's own copy of the file governs, in the pin's order.

Each copy is named first as `tracked` maps its pinned path, where git
tracks it through the root's symlinks, then by its pinned path where
that differs (#807). One `tracked` maps to nothing, or to a name no line
can spell literally, is named by its pinned path alone."""
tracked = tracked or {}
return list(dict.fromkeys(
name for path in shape_files
if not path.startswith(f"{collision_dir}/")
for name in (_tracked_or_pinned(path, tracked.get(path)), path)))


def _tracked_or_pinned(pinned: str, tracked: str | None) -> str:
"""`tracked` where a block line can name it, else `pinned`."""
return tracked if tracked and EOL_PATH_RE.fullmatch(tracked) else pinned


def eol_block(paths) -> str:
Expand Down Expand Up @@ -841,11 +874,25 @@ def _append_eol_block(target: Path, result: "Materialized",
if path.is_symlink() or not path.is_file() \
or _tracked_as_link(target, ATTRIBUTES):
return
paths = eol_block_paths(result.shape_files, collision_dir)
paths = eol_block_paths(result.shape_files, collision_dir,
{rel: _tracked_at(target, rel)
for rel in result.shape_files})
path.write_bytes(with_eol_block(path.read_bytes(), paths))
result.eol_block = paths


def _tracked_at(target: Path, rel: str) -> str | None:
"""Where git tracks the file written at `rel` under `target`: `rel` with
each symlink on the way followed as `open()` followed them when it was
written (`_followed`), so `ci/workflows/validate.yml` for a copy written
through `.github -> ci` (#807). None where they lead out of `target` or
cannot be followed."""
try:
return "/".join(_followed(target, rel, _ticked))
except Refusal:
return None


def collision_follow_up(intended: str, actual: str,
beside: dict | None = None, *,
attributes_link: bool = False) -> str:
Expand Down Expand Up @@ -2722,8 +2769,9 @@ class Materialized:
a refusal in `validate-pins.py`, and rightly.

`eol_block` is the copies named in the block appended to the source's own
`.gitattributes` (#471), and empty when none was: no source file of that
name, one the plan did not keep in the root, or one that is a symlink.
`.gitattributes` (#471), each where git tracks it and, where that
differs, by its pinned path too (#807), and empty when none was: no source file of that name, one the plan did not
keep in the root, or one that is a symlink.
That file is the source's, so it is in neither list above.
"""

Expand Down
Loading
Loading