diff --git a/docs/sources.md b/docs/sources.md index fc74ab17..afbb985a 100644 --- a/docs/sources.md +++ b/docs/sources.md @@ -44,7 +44,7 @@ repository that ships the dotfiles. Gentleman.Dots is hosted at | Upstream | Kind | Verified remote (HTTPS) | Pinned ref | Status | | --- | --- | --- | --- | --- | | ML4W (Hyprland desktop dotfiles) | Desktop base environment | | `3960570f47f4f691c424ff46b387d389a8e69bca` (tag `2.16`) | Pinned — verified against tag 2.16 and remote HEAD | -| Gentleman.Dots | Shell / editor / terminal base configuration | | `6f44b797b016aea92772d8a6d81a5f1bc53a84bb` | Pinned — verified against remote HEAD | +| Gentleman.Dots | Shell / editor / terminal base configuration | | `5b13e07b5a6fde799b65953e92cec323f684408f` | Pinned — verified against remote HEAD | ### Pin mechanism diff --git a/docs/upstream-manifest.json b/docs/upstream-manifest.json index 7f9c8827..0e6dfd9c 100644 --- a/docs/upstream-manifest.json +++ b/docs/upstream-manifest.json @@ -1,7 +1,7 @@ { "version": 1, "provenance": { - "verified_on": "2026-09-28T05:42:13Z", + "verified_on": "2026-09-30T07:27:46Z", "method": "Resolved each upstream HTTPS remote with git ls-remote and recorded the exact returned commit. ML4W is pinned to tag 2.16 (lightweight tag, 3960570f47f4f691c424ff46b387d389a8e69bca), which was also the remote HEAD at verification; Gentleman.Dots is pinned to main HEAD. No ref was ever filled without verification.", "command": "git ls-remote https://github.com/mylinuxforwork/dotfiles.git HEAD refs/tags/2.16; git ls-remote https://github.com/Gentleman-Programming/Gentleman.Dots.git HEAD refs/heads/main" }, @@ -11,14 +11,14 @@ "url": "https://github.com/mylinuxforwork/dotfiles.git", "status": "pinned", "pinned_ref": "3960570f47f4f691c424ff46b387d389a8e69bca", - "verified_on": "2026-09-28T05:42:13Z" + "verified_on": "2026-09-30T07:27:46Z" }, "gentleman-dots": { "name": "Gentleman.Dots (shell / editor / terminal base configuration)", "url": "https://github.com/Gentleman-Programming/Gentleman.Dots.git", "status": "pinned", - "pinned_ref": "6f44b797b016aea92772d8a6d81a5f1bc53a84bb", - "verified_on": "2026-09-28T05:42:13Z" + "pinned_ref": "5b13e07b5a6fde799b65953e92cec323f684408f", + "verified_on": "2026-09-30T07:27:46Z" } }, "owned_paths": {} diff --git a/odd/tasks/upstream-bump-2026-09-30.md b/odd/tasks/upstream-bump-2026-09-30.md new file mode 100644 index 00000000..66aee89b --- /dev/null +++ b/odd/tasks/upstream-bump-2026-09-30.md @@ -0,0 +1,50 @@ +# Upstream bump 2026-09-30 (Gentleman.Dots pin, pin check, Herdr 0.9.3) + +## Objective + +Keep the pinned upstreams and the installed tools current ("vanguardia") without breaking the +Dreamcoder overlay: verify what changed upstream before bumping, and record the evidence. + +## Findings (checked 2026-09-30) + +- ML4W: latest tag `2.16` (pinned). Remote `HEAD` is now `328351d`, ahead of the tag with + unreleased commits; the project pins releases, so this is reported as drift only. +- Gentleman.Dots: one new commit `5b13e07` (`fix(nvim): use valid snacks.picker name for + obsidian`, 2 lines in `GentlemanNvim/nvim/lua/plugins/obsidian.lua`). `DreamcoderNvim` does + `dofile` of Gentleman's `init.lua`, so the fix reaches Neovim once the local checkout moves. +- Herdr: latest `v0.9.3` (2026-09-29), installed `0.9.1`. The official release asset validates + the repo's 0.9.1 dark and light variants with `herdr config check` (`config: ok`). +- `scripts/upstream-diff.py --check-pins` could not verify any pin that was no longer the remote + `HEAD`: it ran `git ls-remote `, which matches ref names, never hashes, so it always + exited 2. It went unnoticed while every pin equalled `HEAD`. + +## Constraints + +- Never install or restart herdr while the user's server has open panes; use a scratch copy of + the release asset for evidence. +- Commit messages: lowercase subject, body lines <= 100 (commitlint). One PR per work unit. +- TDD: off (no project configuration); each behaviour change has a test that fails without it. + +## Tasks + +- [x] U1 — Pin Gentleman.Dots to `5b13e07` (manifest, sources table, test fixture) and fast-forward + the local `~/Gentleman.Dots` checkout. Route: inline. +- [x] U2 — `--check-pins` verifies reachability by fetching the pinned hash into a throwaway bare + repo (the mechanism `_diff_upstream` already uses); tests forbid the ref-name lookup. Live + result: Gentleman.Dots current, ML4W stale (drift, pin reachable). Route: inline. +- [ ] U3 — Herdr 0.9.3 profile and evidence, generated variants, docs. Route: delegated writer + (separate PR). + +## Acceptance criteria + +- `python3 scripts/upstream-diff.py --check-pins` exits 0 against the real remotes and reports + Gentleman.Dots current and ML4W stale-but-reachable. +- pytest, bats, ruff, mypy green; `verify-repo-sync.py` ok. + +## Progress + +- U1 and U2 verified: pytest exit 0, bats 192 ok, ruff and mypy clean. + +## Next step + +U3 (agent running in its own worktree), then record its evidence here. diff --git a/scripts/upstream-diff.py b/scripts/upstream-diff.py index 27cd01dc..58e64eb6 100755 --- a/scripts/upstream-diff.py +++ b/scripts/upstream-diff.py @@ -280,6 +280,33 @@ def _unpinned_result(upstream: dict[str, Any]) -> dict[str, Any]: } +def _require_pin_fetchable(name: str, url: str, pin: str) -> None: + """Fail closed unless the pinned commit can still be fetched by hash. + + `git ls-remote ` cannot do this: it matches ref names, never hashes, so it + exits 2 for every pin that is no longer the remote HEAD. Fetching the hash into a + throwaway bare repo (as _diff_upstream does) is the check that reflects reality. + """ + temp_dir: Path | None = None + try: + temp_dir = Path( + tempfile.mkdtemp(prefix="dreamcoder-upstream-pin-", dir=str(_system_temp_base())) + ) + _run_git(["git", "init", "--bare", "--quiet", str(temp_dir)], ROOT, GIT_TIMEOUT) + _run_git( + ["git", "-C", str(temp_dir), "fetch", "--no-tags", "--depth=1", url, pin], + ROOT, + FETCH_TIMEOUT, + ) + except GitError as exc: + raise GitError( + f"pinned ref {pin} for {name} is no longer reachable on the remote: {exc}" + ) from exc + finally: + if temp_dir is not None: + shutil.rmtree(temp_dir, ignore_errors=True) + + def _check_pin(name: str, upstream: dict[str, Any]) -> dict[str, Any]: """Verify the pinned ref against the remote HEAD (drift is report-only).""" url = upstream["url"] @@ -297,10 +324,7 @@ def _check_pin(name: str, upstream: dict[str, Any]) -> dict[str, Any]: "note": "pinned ref matches remote HEAD", } if head != pin: - reach_out = _run_git(["git", "ls-remote", "--exit-code", url, pin], ROOT, LSREMOTE_TIMEOUT) - reach = reach_out.decode("utf-8", errors="replace").split("\t", 1)[0].strip() - if not SHA_REF_RE.match(reach): - raise GitError(f"pinned ref {pin} for {name} is no longer reachable on the remote") + _require_pin_fetchable(name, url, pin) result["status"] = "stale" result["note"] = ( f"remote HEAD {head} differs from pinned ref {pin}; pin still reachable — " diff --git a/tests/test_upstream_diff.py b/tests/test_upstream_diff.py index 910ef4a5..903d8b25 100644 --- a/tests/test_upstream_diff.py +++ b/tests/test_upstream_diff.py @@ -438,11 +438,19 @@ def test_check_pins_current(env, capsys): assert len(fake.calls) == 1 +def _reachable_pin_git(module: ModuleType) -> FakeGit: + """HEAD moved past the pin; the pinned commit is still fetchable by hash.""" + fake = FakeGit(module) + fake.when(lambda a: "ls-remote" in a and a[-1] == "HEAD", f"{HEAD}\tHEAD\n".encode()) + fake.when(lambda a: "init" in a, b"") + fake.when(lambda a: "fetch" in a and a[-1] == PIN, b"") + fake.when(lambda a: "cat-file" in a and "-e" in a, b"") + return fake + + def test_check_pins_stale_reports_drift(env, capsys): _write_manifest(env, _manifest()) - fake = FakeGit(env.module) - fake.when(lambda a: "ls-remote" in a and a[-1] == "HEAD", f"{HEAD}\tHEAD\n".encode()) - fake.when(lambda a: "ls-remote" in a and a[-1] == PIN, f"{PIN}\trefs/heads/main\n".encode()) + fake = _reachable_pin_git(env.module) _stub_git(env.module, fake) assert env.module.main(["--check-pins", "--json"]) == 0 # drift is report-only report = json.loads(capsys.readouterr().out) @@ -452,11 +460,23 @@ def test_check_pins_stale_reports_drift(env, capsys): assert "drift" in report["upstreams"]["ml4w"]["note"] +def test_check_pins_never_looks_a_hash_up_as_a_ref_name(env, capsys): + # `git ls-remote ` matches ref NAMES, never hashes, so it always exits 2 for a + # pin that is no longer the remote HEAD. Reachability must be checked by fetching the hash. + _write_manifest(env, _manifest()) + fake = _reachable_pin_git(env.module) + _stub_git(env.module, fake) + assert env.module.main(["--check-pins", "--json"]) == 0 + assert not any("ls-remote" in call and call[-1] == PIN for call in fake.calls) + assert any("fetch" in call and call[-1] == PIN for call in fake.calls) + + def test_check_pins_unreachable_pin_fails_closed(env, capsys): _write_manifest(env, _manifest()) fake = FakeGit(env.module) fake.when(lambda a: "ls-remote" in a and a[-1] == "HEAD", f"{HEAD}\tHEAD\n".encode()) - fake.when(lambda a: "ls-remote" in a and a[-1] == PIN, b"") # pin no longer advertised + fake.when(lambda a: "init" in a, b"") + fake.when(lambda a: "fetch" in a, env.module.GitError("upload-pack: not our ref")) _stub_git(env.module, fake) assert env.module.main(["--check-pins", "--json"]) == 1 captured = capsys.readouterr() diff --git a/tests/test_verify_repo_sync.py b/tests/test_verify_repo_sync.py index d757d524..e5b0da49 100644 --- a/tests/test_verify_repo_sync.py +++ b/tests/test_verify_repo_sync.py @@ -236,7 +236,7 @@ def _base_manifest() -> dict: "name": "Gentleman.Dots (shell / editor / terminal base configuration)", "url": "https://github.com/Gentleman-Programming/Gentleman.Dots.git", "status": "pinned", - "pinned_ref": "6f44b797b016aea92772d8a6d81a5f1bc53a84bb", + "pinned_ref": "5b13e07b5a6fde799b65953e92cec323f684408f", "verified_on": "2026-08-10T01:27:49Z", }, },