fix(state): auto-commit died on dead legacy pathspecs after #107
Since d8e1a13 removed theme-state.json, auto_commit's fixed pathspec
(state.json + both legacy names) made `git commit` abort — unlike
`git diff`, commit errors on a pathspec that matches nothing — so every
per-mutation settings commit was silently "skipped" and state.json
stayed dirty until a lifecycle sweep caught it. Filter the legacy names
down to those git still knows (index or HEAD — a migration's staged
`git rm` still rides along). checks.auto-theme now runs its state dir
as a git repo with autoCommit on and asserts each set/apply lands as
its own commit leaving nothing dirty — the exact post-#107 repo shape
that used to abort. state-sync 0.5.0 → 0.5.1.
V2: nix build .#checks.x86_64-linux.auto-theme (KVM VM run, passed,
including the new git assertions); the built tool was also exercised
against a clone of a real post-#107 machine flake — the set
auto-committed and the tree stayed clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
33
flake.nix
33
flake.nix
@@ -2204,10 +2204,14 @@
|
|||||||
# `home-manager switch` is stubbed via NOMARCHY_REBUILD (that
|
# `home-manager switch` is stubbed via NOMARCHY_REBUILD (that
|
||||||
# generic apply path is exercised by every manual theme change);
|
# generic apply path is exercised by every manual theme change);
|
||||||
# this isolates the auto decision + apply + rebuild-trigger.
|
# this isolates the auto decision + apply + rebuild-trigger.
|
||||||
|
# The state dir is a git repo with autoCommit on, so every set/apply
|
||||||
|
# also exercises auto_commit against a repo that has ONLY state.json
|
||||||
|
# — the post-#107 shape whose dead legacy pathspecs once aborted
|
||||||
|
# every commit silently.
|
||||||
auto-theme = pkgs.testers.runNixOSTest {
|
auto-theme = pkgs.testers.runNixOSTest {
|
||||||
name = "nomarchy-auto-theme";
|
name = "nomarchy-auto-theme";
|
||||||
nodes.machine = { pkgs, ... }: {
|
nodes.machine = { pkgs, ... }: {
|
||||||
environment.systemPackages = [ pkgs.nomarchy-state-sync ];
|
environment.systemPackages = [ pkgs.nomarchy-state-sync pkgs.git ];
|
||||||
time.timeZone = "UTC"; # naive HH:MM == the clock we set
|
time.timeZone = "UTC"; # naive HH:MM == the clock we set
|
||||||
};
|
};
|
||||||
testScript = ''
|
testScript = ''
|
||||||
@@ -2220,6 +2224,14 @@
|
|||||||
machine.succeed(
|
machine.succeed(
|
||||||
"echo '{\"slug\":\"summer-day\"}' > /root/.nomarchy/state.json"
|
"echo '{\"slug\":\"summer-day\"}' > /root/.nomarchy/state.json"
|
||||||
)
|
)
|
||||||
|
# Git-tracked like a real downstream flake — no legacy
|
||||||
|
# theme-state.json/theme.json anywhere in its history.
|
||||||
|
machine.succeed(
|
||||||
|
"git -C /root/.nomarchy init -q"
|
||||||
|
" && git -C /root/.nomarchy add state.json"
|
||||||
|
" && git -C /root/.nomarchy -c user.name=t -c user.email=t@t"
|
||||||
|
" commit -qm init"
|
||||||
|
)
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
"printf '#!/bin/sh\\necho switch >> /root/rebuilds\\n' > /root/rebuild"
|
"printf '#!/bin/sh\\necho switch >> /root/rebuilds\\n' > /root/rebuild"
|
||||||
" && chmod +x /root/rebuild"
|
" && chmod +x /root/rebuild"
|
||||||
@@ -2229,14 +2241,24 @@
|
|||||||
"NOMARCHY_DEFAULT_THEMES=${pkgs.nomarchy-default-themes} "
|
"NOMARCHY_DEFAULT_THEMES=${pkgs.nomarchy-default-themes} "
|
||||||
"NOMARCHY_REBUILD=/root/rebuild ")
|
"NOMARCHY_REBUILD=/root/rebuild ")
|
||||||
|
|
||||||
# Pair + schedule (setup writes only, no rebuild).
|
# Pair + schedule (setup writes only, no rebuild). autoCommit
|
||||||
for kv in ["settings.autoTheme.enable true",
|
# first, so every following write must land as its own commit.
|
||||||
|
for kv in ["settings.autoCommit true",
|
||||||
|
"settings.autoTheme.enable true",
|
||||||
"settings.autoTheme.day summer-day",
|
"settings.autoTheme.day summer-day",
|
||||||
"settings.autoTheme.night summer-night",
|
"settings.autoTheme.night summer-night",
|
||||||
"settings.autoTheme.sunrise 07:00",
|
"settings.autoTheme.sunrise 07:00",
|
||||||
"settings.autoTheme.sunset 20:00"]:
|
"settings.autoTheme.sunset 20:00"]:
|
||||||
machine.succeed(env + "nomarchy-state-sync --quiet set " + kv + " --no-switch")
|
machine.succeed(env + "nomarchy-state-sync --quiet set " + kv + " --no-switch")
|
||||||
|
|
||||||
|
# Every `set` above auto-committed (the regression: dead legacy
|
||||||
|
# pathspecs made `git commit` abort, skipping ALL of these).
|
||||||
|
machine.succeed(
|
||||||
|
"git -C /root/.nomarchy log --format=%s"
|
||||||
|
" | grep -qF 'set settings.autoTheme.sunset'"
|
||||||
|
)
|
||||||
|
machine.succeed("git -C /root/.nomarchy diff --quiet HEAD -- state.json")
|
||||||
|
|
||||||
def slug():
|
def slug():
|
||||||
return machine.succeed(env + "nomarchy-state-sync get slug").strip()
|
return machine.succeed(env + "nomarchy-state-sync get slug").strip()
|
||||||
def rebuilds():
|
def rebuilds():
|
||||||
@@ -2253,6 +2275,11 @@
|
|||||||
machine.succeed(env + "nomarchy-state-sync auto")
|
machine.succeed(env + "nomarchy-state-sync auto")
|
||||||
assert slug() == "summer-night", f"post-sunset should be night, got {slug()}"
|
assert slug() == "summer-night", f"post-sunset should be night, got {slug()}"
|
||||||
assert rebuilds() == 1, f"sunset should trigger one rebuild, got {rebuilds()}"
|
assert rebuilds() == 1, f"sunset should trigger one rebuild, got {rebuilds()}"
|
||||||
|
# The switch auto-committed too — nothing left dirty behind it.
|
||||||
|
machine.succeed(
|
||||||
|
"git -C /root/.nomarchy log --format=%s | grep -qF 'apply theme'"
|
||||||
|
)
|
||||||
|
machine.succeed("git -C /root/.nomarchy diff --quiet HEAD -- state.json")
|
||||||
|
|
||||||
# Another tick still after sunset → idempotent, no extra rebuild.
|
# Another tick still after sunset → idempotent, no extra rebuild.
|
||||||
machine.succeed("date -s '2026-01-02 20:06:00'")
|
machine.succeed("date -s '2026-01-02 20:06:00'")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
stdenvNoCC.mkDerivation {
|
stdenvNoCC.mkDerivation {
|
||||||
pname = "nomarchy-state-sync";
|
pname = "nomarchy-state-sync";
|
||||||
version = "0.5.0";
|
version = "0.5.1";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
|
|||||||
@@ -189,8 +189,17 @@ def auto_commit(message: str) -> None:
|
|||||||
if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None:
|
if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None:
|
||||||
return
|
return
|
||||||
git = ["git", "-C", str(FLAKE_DIR)]
|
git = ["git", "-C", str(FLAKE_DIR)]
|
||||||
# Pathspec: preferred name plus any leftover legacy file still staged.
|
# Pathspec: preferred name plus any legacy file git still knows — in the
|
||||||
paths = [STATE_NAME, *LEGACY_STATE_NAMES]
|
# index or in HEAD (a migration's staged `git rm` needs the pathspec to
|
||||||
|
# ride along). Names known to neither MUST be dropped: unlike `git diff`,
|
||||||
|
# `git commit` aborts on a pathspec that matches nothing, which silently
|
||||||
|
# killed every auto-commit once the #107 rename removed the legacy files.
|
||||||
|
def known(name: str) -> bool:
|
||||||
|
return subprocess.run(git + ["ls-files", "--error-unmatch", "--", name],
|
||||||
|
capture_output=True).returncode == 0 \
|
||||||
|
or subprocess.run(git + ["cat-file", "-e", f"HEAD:{name}"],
|
||||||
|
capture_output=True).returncode == 0
|
||||||
|
paths = [STATE_NAME] + [n for n in LEGACY_STATE_NAMES if known(n)]
|
||||||
# No-op when the file already matches HEAD (a `set` to the same value).
|
# No-op when the file already matches HEAD (a `set` to the same value).
|
||||||
# On a repo with no commits yet this diff errors — then just commit.
|
# On a repo with no commits yet this diff errors — then just commit.
|
||||||
if subprocess.run(git + ["diff", "--quiet", "HEAD", "--"] + paths,
|
if subprocess.run(git + ["diff", "--quiet", "HEAD", "--"] + paths,
|
||||||
|
|||||||
Reference in New Issue
Block a user