fix(issue-21): review ec50fbe7 R6 - the unpushed check covers HEAD and every local ref (tags included, stash excluded) in both submodule checks
Both unpushed checks in lib_py/worktree.py - `submodule foreach` over the initialised checkouts and the direct `--git-dir` walk over the repositories under the worktree's modules/ - used `HEAD --branches --not --remotes`. A commit made on a submodule's detached HEAD and kept only by a tag is reachable from neither, the top-level status stays clean, and worktree_remove.sh then deleted the repository with its single --force (exit 0). Both checks now share one rev list, UNPUSHED_REVS = HEAD --exclude=refs/stash --all --not --remotes: HEAD and every local ref (branches, tags, notes, ...) minus remote-tracking refs. refs/stash stays out of it because the stash check (stash list / reflog) owns it, so a stash is reported once, as 'stashes'. As intended, a local-only note is now protected too. The stash, error, partial repository and dirty checks are unchanged. Tests W6l (tag-only commit; initialised submodule and deinit'd repository) and W6m (annotated tag). The helper header, the stop SKILL.md and the design doc state the wider rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
This commit is contained in:
1 parent
c7c559b7aa
commit
25f4be3359
5 files changed
+59
-10
No files matched your search
@@ -604,3 +604,46 @@ def test_w6k_files_in_an_uninitialised_submodule_path_refuse_the_init(ctl_sub):
|
||||
gd = _git(wt, "rev-parse", "--absolute-git-dir").stdout.strip()
|
||||
assert not os.path.exists(os.path.join(gd, "modules"))
|
||||
assert _git(main, "config", "--get", "submodule.sm.url", check=False).returncode != 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# review ec50fbe7 (R6): a submodule commit kept only by a tag
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _tag_only_commit(sm: Path, annotated: bool = False) -> str:
|
||||
"""Commit on the submodule's detached HEAD, keep it only by a tag, go back
|
||||
to the recorded commit (the top level stays clean). Returns the commit."""
|
||||
orig = _git(sm, "rev-parse", "HEAD").stdout.strip()
|
||||
(sm / "tagged.txt").write_text("kept only by a tag\n")
|
||||
_git(sm, "add", "tagged.txt")
|
||||
_git(sm, "commit", "-q", "-m", "tag-only work")
|
||||
local = _git(sm, "rev-parse", "HEAD").stdout.strip()
|
||||
_git(sm, "tag", *(["-a", "-m", "keep"] if annotated else []), "saved-work")
|
||||
_git(sm, "checkout", "-q", orig)
|
||||
return local
|
||||
|
||||
|
||||
def _remove_refuses_tag_only_commit(main: Path, annotated: bool, deinit: bool) -> None:
|
||||
wt = _wt(main)
|
||||
assert _prepare(main, wt, "--submodules", env=FILE_OK).returncode == 0
|
||||
local = _tag_only_commit(wt / "sm", annotated)
|
||||
gd = _git(wt / "sm", "rev-parse", "--absolute-git-dir").stdout.strip()
|
||||
assert _git(wt, "status", "--porcelain", "--ignore-submodules=none").stdout == ""
|
||||
if deinit:
|
||||
_git(wt, "submodule", "deinit", "-q", "sm")
|
||||
res = _run(main, REM, "--path", str(wt))
|
||||
assert res.returncode == 3 and "'unpushed'" in res.stderr, res.stdout + res.stderr
|
||||
assert os.path.isdir(gd)
|
||||
assert _git(gd, "rev-parse", "saved-work^{commit}").stdout.strip() == local
|
||||
|
||||
|
||||
@pytest.mark.parametrize("deinit", [False, True])
|
||||
def test_w6l_tag_only_submodule_commit_is_kept(ctl_sub, deinit):
|
||||
"""R6: HEAD and --branches miss a commit only a tag reaches; both checks
|
||||
(initialised checkout via foreach, retained repository via --git-dir) use
|
||||
the same rev list and refuse."""
|
||||
_remove_refuses_tag_only_commit(ctl_sub, annotated=False, deinit=deinit)
|
||||
|
||||
|
||||
def test_w6m_annotated_tag_only_submodule_commit_is_kept(ctl_sub):
|
||||
_remove_refuses_tag_only_commit(ctl_sub, annotated=True, deinit=False)
|
||||
Reference in new issue
Block a user