Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
daff221
shape-doctor.py: a root's own `contracts/shape-pin.yaml` that does no…
brettheap Oct 11, 2026
092a5f9
README.md: take back the shape-pin sentence added to the exit-3 parag…
brettheap Oct 11, 2026
7c11c62
shape-doctor.py: a root's own `contracts/shape-pin.yaml` that does no…
brettheap Oct 12, 2026
9f64bd0
tests/test_shape_doctor.py: a bad pin staged where HEAD holds none is…
brettheap Oct 12, 2026
462baf1
shape-doctor.py: the `manifest kinds` row puts back a file a tool wro…
brettheap Oct 12, 2026
f80179c
shape-doctor.py: a merge's NEXT takes whole only the pin and the copi…
brettheap Oct 12, 2026
8badae0
tests/test_shape_doctor.py: the doctor leaves a stat-dirty index byte…
brettheap Oct 12, 2026
b312ad1
Merge 8badae0 (#267 fix round 2) into fix/doctor-manifest-kinds-unrea…
brettheap Oct 12, 2026
ebffb08
tests/test_shape_doctor.py: a leg pin conflicted beside `README.md` i…
brettheap Oct 12, 2026
2745890
shape-doctor.py: the `manifest kinds` row puts back from git only a p…
brettheap Oct 12, 2026
ef978ed
Merge branch 'main' into fix/doctor-root-shape-pin-unreadable
brettheap Oct 12, 2026
f6dd11a
Merge branch 'main' into fix/doctor-root-shape-pin-unreadable
brettheap Oct 12, 2026
ce2ce75
Merge f6dd11a (#267's branch, with main through 5678f68) into fix/doc…
brettheap Oct 12, 2026
30c963c
shape-doctor.py: a copy the shape pin has a row for, and a neutral pr…
Oct 12, 2026
b88aa65
Merge decff87 (#267, squash-landed on main) into fix/doctor-manifest-…
Oct 12, 2026
a5563c3
Merge main d6df36b into fix/doctor-manifest-kinds-unreadable-pin: #40…
Oct 12, 2026
3af2829
Merge main 437a360 into fix/doctor-manifest-kinds-unreadable-pin: #34…
Oct 12, 2026
65def40
shape-doctor.py: a neutral product pin's reason no longer ends in its…
Oct 12, 2026
c80a0f3
shape-doctor.py: a conflicted shape pin's NEXT takes the pin alone an…
Oct 12, 2026
565f920
Merge #358's head 65def40 into fix/485-doctor-conflicted-pin-and-copy…
Oct 12, 2026
c637722
Merge branch 'main' into fix/doctor-manifest-kinds-unreadable-pin
brettheap Oct 12, 2026
eba8d55
Merge #358's final head c637722 into fix/485-doctor-conflicted-pin-an…
Oct 13, 2026
8ca30eb
Merge #358's squash 5a25387 into fix/485-doctor-conflicted-pin-and-co…
Oct 13, 2026
e0ef99b
Merge main cc8f2da into fix/485-doctor-conflicted-pin-and-copy, and #…
Oct 13, 2026
af45d7c
shape-doctor.py: the conflicted shape pin's note says `apply --accept…
Oct 13, 2026
85d3e1e
Merge branch 'main' into fix/485-doctor-conflicted-pin-and-copy
brettheap Oct 13, 2026
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
157 changes: 107 additions & 50 deletions shape-doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2363,7 +2363,7 @@
fixes.append(FAMILY_BESIDE_FIX)
command = "; ".join(fixes)
if unreadable:
restore, command = unreadable_next(ctx, unreadable)
restore, command = unreadable_next(ctx, unreadable, detail)
detail.update({"restore": restore, "refusals": refusals})
return Row("manifest-kinds", LABEL_MANIFEST_KINDS, FINDING,
"; ".join(parts) + aside, command, detail)
Expand Down Expand Up @@ -2515,7 +2515,7 @@
THE ROWS, NOT A YES, for the shape pin (review of PR #358): `rev` may
be an index stage, `:2` or `:3`, and the rows of the side of a merge
`pin_restore` takes whole are the copies that side's tool wrote, so
they are what it names with the pin.
they are the conflicted files its note gives a person to merge (#485).
"""
blob = git_answer(["cat-file", "blob", f"{rev}:./{pin}"],
ctx.root, binary=True)
Expand Down Expand Up @@ -2544,23 +2544,43 @@
these that applies decides, as `detail["restore"]`:

unmerged a merge left the pin conflicted. `checkout --ours`, or
`--theirs` where only that side holds a pin, of every
conflicted path the shape writes WHOLE -- the pin, and
the copies that side's pin has `files:` rows for, their
collision copies included -- because a merge of two
`update-shape.py apply` runs conflicts in them together,
and one side taken whole is one tool's output. Those rows
and not this standard's copy lists: a file the pin has no
row for is not a shape copy, though the lists name its
path (review of PR #358).
The manifest and README.md are never named, though `apply`
writes lines of them: every other line is the project's
own, and one side taken whole drops the other side's
edits, the hunks git merged cleanly included (review of
PR #267). Where either is conflicted the note says a
person merges it, and `detail["left_for_a_person"]` lists
it. A file of the project's own is never named: its merge
is theirs.
`--theirs` where only that side holds a pin, of THE PIN
ALONE, which only a tool writes, so one side taken whole
is one tool's output.
A copy is never named with it (#485), though a merge of
two `update-shape.py apply` runs conflicts in the two
together: a copy can hold an edit of the project, kept by
`apply --accept-local` on one side, and taken whole the
other side's copy dropped it, the hunks git merged
cleanly with it, and the doctor then said COMPLIANT. The
copies are the files the taken side's pin has `files:`
rows for, their collision copies included -- those rows
and not this standard's copy lists, since a file the pin
has no row for is not a shape copy, though the lists
name its path (review of PR #358). Where one of them is
conflicted the note says a person merges it by hand,
keeping the hunks git merged, as a copy conflicted on its
own is merged, and that `update-shape.py apply
--accept-local` on a copy merged unlike the pin re-pins
it from the merge. The `shape currency` row names it
where `check` calls the copy `locally-modified`, as it
does with the update side's pin taken. With the
person's side's pin taken, behind the standard the copy
was merged from, `check` calls it `both`, and the row's
`apply` is refused `update-conflict` and asks for it
(review of PR #496). The row never names it for `both`:
AGENTS.md's rule 3 is never to pass it to make a refusal
go away, and nothing here tells a copy merged by hand
from one nobody merged.
The manifest and README.md are never named either, though
`apply` writes lines of them: every other line is the
project's own, and one side taken whole drops the other
side's edits, the hunks git merged cleanly included
(review of PR #267). Where either is conflicted the note
says a person merges it.
`detail["left_for_a_person"]` lists every file the note
gives a person. A file of the project's own is never
named: its merge is theirs.
uncommitted HEAD holds a pin that reads, and the tree or the index
differs from it. `checkout HEAD`, which drops a change no
tool of this standard leaves uncommitted.
Expand Down Expand Up @@ -2597,10 +2617,10 @@
declares, for a leg or a neutral product, or a copy the shape pin has a
row for -- which is how the `manifest kinds` row answers each one that
does not load. The note says `copy` where the file is not a pin, and
`pin_reads_at` says what reads. Only the shape pin takes more paths with
it in a merge -- the copies `apply` writes in the same commit -- and
only its note names a conflicted manifest or README.md for a person.
Any other file is named alone. A leg pin moves with its gitlink, but
`pin_reads_at` says what reads. Only the shape pin's note names other
paths of a merge, for a person to merge by hand: the copies `apply`
writes in the same commit, and a conflicted manifest or README.md.
Every pin is named alone. A leg pin moves with its gitlink, but
`checkout --ours` on a conflicted gitlink exits 0 and leaves all three
stages where they were, so naming it would be a command that changes
nothing. Finishing the merge takes the gitlink, and the `pins` row then
Expand Down Expand Up @@ -2640,7 +2660,6 @@
if xy in UNMERGED_STATUS:
us = ctx.update_shape()
kind = us.root_kind(root)
whole = {pin}
# `<mode> SP <object> SP <stage> TAB <path>`, one record per stage.
stages: dict[str, set] = {}
listing = git_answer(["ls-files", "-u", "-z"], root, binary=True)
Expand All @@ -2654,33 +2673,53 @@
("--theirs", b"3") if b"3" in pin_stages else
(None, None))
if side:
if pin == SHAPE_PIN:
whole.update(pin_at(ctx, f":{stage.decode()}") or ())
note = ("--theirs takes the other side. Take one side whole"
if b"3" in pin_stages and side == "--ours"
note = (f"--theirs takes the other side. Take one side of the "
f"{what} whole" if b"3" in pin_stages and side == "--ours"
else f"only that side of the merge holds a {what}. Take "
"it whole")
named = sorted(name for name, held in stages.items()
if name in whole and stage in held)
left = (sorted(name for name in (kind.manifest, us.README)
if name in stages) if pin == SHAPE_PIN else [])
then = ", then finish"
if left:
lines, copies = [], []
if pin == SHAPE_PIN:
lines = sorted(name for name in (kind.manifest, us.README)
if name in stages)
copies = sorted(name for name in set(
pin_at(ctx, f":{stage.decode()}") or ())
if name in stages)
then, after = ", then finish", ""
if lines or copies:
if detail is not None:
detail["left_for_a_person"] = left
says = ("are conflicted too and hold lines of the project, "
"so a person merges them" if len(left) > 1 else
"is conflicted too and holds lines of the project, "
"so a person merges it")
then = (f". {' and '.join(left)} {says}, keeping the lines "
"update-shape.py writes from the side taken here. "
"Then finish")
detail["left_for_a_person"] = sorted(lines + copies)
said = []
if lines:
said.append(
f"{' and '.join(lines)} "
+ ("are conflicted too and hold lines of the "
"project, so a person merges them" if len(lines) > 1
else "is conflicted too and holds lines of the "
"project, so a person merges it")
+ ", keeping the lines update-shape.py writes from "
"the side taken here")
if copies:
said.append(
f"{' and '.join(copies)} "
+ ("are copies conflicted too" if len(copies) > 1
else "is a copy conflicted too")
+ f". {BY_HAND_WHY['copy']}, so neither side is "
"taken whole: a person merges "
+ ("each" if len(copies) > 1 else "it")
+ " by hand, keeping the hunks git merged")
after = (". Where a copy as merged differs from the pin, "
"update-shape.py apply --accept-local on that "
"copy re-pins it from the merge. The shape "
"currency row names it where update-shape.py "
"check calls the copy locally-modified, and "
"where check calls it both, the apply the row "
"names is refused as update-conflict and asks "
"for --accept-local on that path")
then = ". " + ". ".join(said) + ". Then finish"
return "unmerged", (
f"{git_c} checkout {side} -- "
+ " ".join(quote_arg(name) for name in named)
+ f" # {note} for every path named and never a "
f"hand-merged {what}{then} the merge as git says and run "
"this doctor again")
f"{git_c} checkout {side} -- {quote_arg(pin)} # {note} and "
f"never a hand-merged {what}{then} the merge as git says and "
f"run this doctor again{after}")
elif pin_reads_at(ctx, "HEAD", pin):
if xy and shown:
return "uncommitted", (
Expand Down Expand Up @@ -2853,7 +2892,8 @@
"once they load")


def unreadable_next(ctx: Context, unreadable: list) -> tuple[dict, str]:
def unreadable_next(ctx: Context, unreadable: list,

Check failure on line 2895 in shape-doctor.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoShape&issues=AaEyYYYm-M-4VfiuHuzi&open=AaEyYYYm-M-4VfiuHuzi&pullRequest=496
detail: dict | None = None) -> tuple[dict, str]:
"""`(restore, command)` for the `manifest kinds` row's files that do not
load (#291): each file's case, and the one command the row prints.

Expand All @@ -2876,16 +2916,33 @@
the file, and its note says that what the file should say is theirs to
decide. #205's `manifest` row answers a `project.yaml` that does not
load the same way: the gate that refuses it, and that note.

WHAT THE SHAPE PIN'S MERGE LEAVES TO A PERSON IS SAID HERE TOO (#485).
While the shape pin does not read, no row says what is a copy, so a
copy conflicted beside it is not `tool_written`. The side of the merge
`pin_restore` takes does say, and its note gives each such copy to a
person. That copy's case is `unmerged`, the one it has once the pin
reads, and not `project`, and `detail` gets the `left_for_a_person`
the `shape currency` row's has, so both rows say the same of the same
command.
"""
restore: dict[str, str] = {}
commands: dict[str, str] = {}
held: dict = {}
for rel in unreadable:
if tool_written(ctx, rel):
restore[rel], found = pin_restore(ctx, pin=rel)
restore[rel], found = pin_restore(
ctx, held if rel == SHAPE_PIN else None, pin=rel)
if found:
commands[rel] = found
else:
restore[rel] = "project"
left = held.get("left_for_a_person", [])
for rel in left:
if restore.get(rel) == "project":
restore[rel] = "unmerged"
if left and detail is not None:
detail["left_for_a_person"] = left
command = commands.get(SHAPE_PIN) or next(iter(commands.values()), None)
if command is None:
command = (f"{PYTHON} {quote_arg(ctx.shape / 'shape-doctor.py')} "
Expand Down Expand Up @@ -2922,7 +2979,7 @@
THE NEXT PUTS BACK THE PIN A TOOL WROTE, NEVER A FETCH. `pin_restore`
names the git command from the pin's own state, and run as printed it
ends at `COMPLIANT` or at a row that reads the pin, but for what its
note leaves to a person: a conflicted manifest or README.md, or an index
note gives a person: a conflicted copy, manifest or README.md, or an index
flag that hides the pin. Naming the tool that refused, `update-shape.py
check`, was a dead end (review of PR #267): it reads the pin with the
same `read_pin`, refuses it the same way, and its remediation is about
Expand Down
Loading
Loading