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
87 changes: 82 additions & 5 deletions scripts/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,34 @@ def _branch_said(family: "Family", verb: str, suggested: str) -> list[str]:
REBASE_HEAD_NAMES = {"rebase-merge": "rebase-merge/head-name",
"rebase-apply": "rebase-apply/head-name"}

#: THE TWO MARKS A REFTABLE KEEPS AS REFS (#684, #612's review E4). A holder
#: whose refs are a reftable (`git init --ref-format=reftable`, git 2.45 and
#: later) keeps `CHERRY_PICK_HEAD` and `REVERT_HEAD` in its ref store while
#: a cherry-pick or a revert is stopped, and in no file; `MERGE_HEAD`,
#: `rebase-merge/` and `sequencer/todo` stay files there (lane 5's probe, on
#: git 2.47.1). The file check found neither, so the verb went on to the
#: commit this check is there to keep it from. Each is asked of the ref store
#: in its own row of `OPERATION_MARKS`, where its file is not there
#: (`_in_ref_store`), and the refusal says git keeps it `IN_THE_REF_STORE`.
#: `MERGE_HEAD` is asked as a file alone, because it is a file on both
#: backends: a stopped merge leaves `MERGE_HEAD` a file on a reftable too
#: (lane 5's probe, git 2.47.1), and git reads `MERGE_HEAD` and `FETCH_HEAD`
#: from their files whatever the backend (`is_pseudo_ref`, git 2.47.1's
#: `refs.c`), so the file is the whole answer. Asking the ref store for it
#: would also stop the verbs on git 2.43, which answers `git show-ref
#: --exists MERGE_HEAD` 1, `failed to look up reference: Success`, where no
#: merge is in progress (measured; git 2.47.1 answers 2 there).
REF_STORE_MARKS = ("CHERRY_PICK_HEAD", "REVERT_HEAD")
IN_THE_REF_STORE = "in its ref store, not as a file"
#: How `git show-ref --exists <name>` (git 2.43 and later) answers where the
#: ref store holds no ref of that exact name: 2, `error: reference does not
#: exist`; 0 where it holds one (both measured on git 2.43, for each mark,
#: with a cherry-pick stopped, a revert stopped and neither). A git before
#: 2.43 knows no `--exists` and exits 129, `error: unknown option `exists'`
#: (measured on git 2.40.4).
SHOW_REF_MISSING = 2
GIT_UNKNOWN_OPTION = 129


def _rebase_unreadable(mark: str) -> tuple:
"""`(finish, abort, effect)` for a `mark` directory that holds no
Expand Down Expand Up @@ -1032,6 +1060,36 @@ def asked(*words: str) -> str:
return [root / line for line in lines]


def _in_ref_store(root: Path, mark: str) -> bool:
"""Whether the holder's ref store holds a ref called EXACTLY `mark`
(`REF_STORE_MARKS`, #684), asked with `git show-ref --exists <mark>`, one
process per mark, since `--exists` takes one name.

THE EXACT NAME. `git rev-parse -q --verify CHERRY_PICK_HEAD`, which the
issue named, also resolves a branch or a tag of that name, and no
operation leaves either: measured on git 2.43 with `git branch
CHERRY_PICK_HEAD` and `git tag REVERT_HEAD`, it answers each with the
commit, and `show-ref --exists` answers 2 for both.

GIT'S OWN FAILURE IS NEVER "NOTHING IN PROGRESS". Only 0 is "held" and
only 2 "not held"; any other exit is a `CommandFailed`, which the verb
prints as it prints every failed git command, exit 2, before its first
write. The one other exit read as "not held" is 129, from a git before
2.43, which knows no `--exists`: such a git cannot open a reftable at
all (`unknown repository extension found: refstorage`, exit 128 at the
`git rev-parse` `_git_paths` asks first, measured on git 2.43 and 2.40.4
with the extension set by hand), so a holder it gets this far in keeps
its refs as files, and the file asked first is the whole answer.
"""
try:
_git_bytes(["show-ref", "--exists", mark], root)
except CommandFailed as exc:
if exc.code in (SHOW_REF_MISSING, GIT_UNKNOWN_OPTION):
return False
raise
return True


def _sequence_act(todo: Path) -> tuple:
"""`(what, finish, abort, effect)` for the series `todo` holds, named from
its first word as `git status` names it, or `SEQUENCE_UNREADABLE`."""
Expand All @@ -1053,18 +1111,24 @@ def _sequence_act(todo: Path) -> tuple:


def _operation_in_progress(
root: Path) -> tuple[str, str, Path, str | None, str, str] | None:
root: Path,
) -> tuple[str, str, Path | None, str | None, str, str] | None:
"""`(what, the mark's name, where it is, how to finish it, how to give it
up, what the partial commit would do)` for the act the holder's git
directory shows unfinished, or None; the first of `OPERATION_MARKS`
that is there, then a `sequencer/`. `finish` is None where git can
continue nothing."""
continue nothing. `where` is None for a mark the holder's ref store holds
and no file does (`REF_STORE_MARKS`, #684): the ref store is asked in the
mark's own row, once its file is not there, so the rows keep their
order and a mark that is a file is named at its path, as before."""
names = [*(mark for mark, *_ in OPERATION_MARKS), AM_MARK, SEQUENCER,
SEQUENCER_TODO, *REBASE_HEAD_NAMES.values()]
marks = dict(zip(names, _git_paths(root, names)))
for mark, what, finish, abort, effect in OPERATION_MARKS:
where = marks[mark]
if not os.path.lexists(where):
if mark in REF_STORE_MARKS and _in_ref_store(root, mark):
return what, mark, None, finish, abort, effect
continue
if mark == "rebase-apply" and os.path.lexists(marks[AM_MARK]):
what, finish, abort, effect = AM_OPERATION
Expand Down Expand Up @@ -1176,6 +1240,16 @@ def _checked_no_operation_in_progress(root: Path, verb: str) -> None:
directory, never of a member's: a merge in a MEMBER's mount is that
checkout's own business.

A REFTABLE KEEPS TWO OF THE MARKS AS REFS (#684, #612's review E4). A
holder whose refs are a reftable holds `CHERRY_PICK_HEAD` and
`REVERT_HEAD` in its ref store and in no file, and the file check found
neither, so a cherry-pick or a revert stopped there was not refused.
Where the file is not there, each is asked of the ref store too, by its
exact name (`_in_ref_store`), and the refusal says git keeps it there
(`IN_THE_REF_STORE`), with the same exit. A mark that is a file is named
at its path, byte for byte as before, and git's own failure to answer is
a failed git command, never "nothing in progress".

WHEN IT IS ASKED (#612's review, N4; #684). Each verb asks it right
after `Family.open` and the checks of its own arguments, so after
`family.yaml` is parsed. The commonest merge conflict in a holder is in
Expand All @@ -1198,10 +1272,12 @@ def _checked_no_operation_in_progress(root: Path, verb: str) -> None:
if found is None:
return
what, mark, where, _, _, effect = found
kept = (IN_THE_REF_STORE if where is None
else f"at {shown_source(where.as_posix())}")
raise Refusal(
"family-operation-in-progress",
f"{what} is unfinished in the family root: git keeps {mark} at "
f"{shown_source(where.as_posix())}. `{verb}` makes its commit with "
f"{what} is unfinished in the family root: git keeps {mark} "
f"{kept}. `{verb}` makes its commit with "
"`git commit -- "
f"<paths>`, and {effect.format(verb=verb)}. No git command that "
"writes was run and nothing was written.",
Expand Down Expand Up @@ -1231,8 +1307,9 @@ def _unparsable_in_an_act(root: Path) -> str:
return ""
what, mark, where, *_ = found
said = _operation_exit(found)
kept = IN_THE_REF_STORE if where is None else f"at {where.as_posix()}"
return (f"Remediation: {what} is unfinished in the family root: git "
f"keeps {mark} at {where.as_posix()}, and where it left "
f"keeps {mark} {kept}, and where it left "
f"{MANIFEST} conflicted, the reader cannot parse the lines git "
"marks the conflict with (`<<<<<<<` reads as a YAML merge key). "
+ said[:1].upper() + said[1:])
Expand Down
5 changes: 3 additions & 2 deletions tests/test_family_holder_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
way out that drops it is named. `member-path`'s remedy says to commit a
row put back by hand, so it no longer leads into this refusal.

#612's review E4 (reftable) is NOT here: this host's git is 2.43, and
the issue asks for it to be measured on git 2.45 or later first.
#612's review E4 (reftable) is NOT here: `CHERRY_PICK_HEAD` and
`REVERT_HEAD` held in a reftable's ref store, and in no file, are
`test_family_operation_reftable.py`'s.

THE PROOF IS #612's, with the helpers of `test_family_refuse_up_front.py`:
exit 2 and one REFUSED paragraph, nothing on stdout (no member was read),
Expand Down
Loading
Loading