Skip to content

fix(config): tolerate unrecognised ini keys and name config faults - #75

Merged
chess-seventh merged 21 commits into
masterfrom
feat/l76-key-skew
Jul 30, 2026
Merged

chess-seventh merged 21 commits into
masterfrom
feat/l76-key-skew

Conversation

@chess-seventh

Copy link
Copy Markdown
Owner

Closes the other axis of the outage 4.17.3 half-fixed. That release made an
unrecognised config section non-fatal; the keys were left as they were,
so a rename would have reproduced the same every-commit-in-eight-repos failure
through a different door.

Measured on 4.17.3 by driving real commits through a real post-commit hook:

config fault before after
unrecognised key nothing at all on stderr, journals normally named on stderr, journals normally
required key missing panicked at src/config.rs:862: Could not get commit_path from config names the config file, [section] key, and the typo that explains it
required key blank (commit_path =) exit 0, journals into the vault root treated as missing, same message
unrenderable format (%Q) opaque error from inside the writer, after creating an empty diary file named at config load, writes nothing
unrecognised section named on stderr unchanged

In every case git commit still exits 0 and the commit stands — the tool runs
post-commit, and git ignores that hook's exit status. That is what makes a
fatal config error safe, and it is now stated in the README.

Decisions

  • Required keys stay fatal. Degrading to "journal nothing, exit 0" would
    make a broken config indistinguishable from a quiet day; the diary would stop
    for weeks unnoticed. The fix is the message, not the severity.
  • [templates] commit_datetime is now honoured. It was required on pain of
    a fatal error and never read — the row's TIME column was hardcoded. Wiring it
    up changes no output for a config already set to %H:%M:%S.

Verification

Four adversarial review rounds; three found a real blocker, each fixed.
Tests are mutation-checked: neutering the format probe, dropping the blank-value
filter, restoring the hardcoded time format, or reverting either call site each
turns the suite red.

Gate: 151 tests, clippy -D warnings, cargo shear, coverage 96.75% (96.24% on
master). tests/hook-gate.sh drives a real commit through a real hook for all
six config cases.

Note: treefmt --fail-on-change fails on .github/workflows/docs.yml. That
drift pre-exists this branch and is untouched by it.

The shared ini and the binary reading it drift apart by design, so config
skew has two axes. Only sections were made tolerant in 4.17.3.

- record both key faults measured through a real post-commit hook: an
  unrecognised key is swallowed in total silence, and a missing required
  key panics with a message that never names the config file
- record the decision: unrecognised keys warn and continue, required keys
  stay fatal because a missing destination must not look like a quiet day
- state the acceptance scenarios that drive the fix, including the
  real-commit gate at the hook boundary

Co-Authored-By: Vulcan <chess7th@pm.me>
A key this binary does not know was swallowed in total silence, so a
misspelt or renamed key applied nothing and said nothing.

- add a KNOWN_KEYS table beside KNOWN_SECTIONS and name every key that
  falls outside it, sorted so hash-map order cannot leak into the output
- warn to stderr as well as the log, because the git hook runs without
  RUST_LOG and would swallow a log-only warning
- keep it non-fatal, for the reason an unknown section is: one ini file is
  shared by every checkout, so a newer config must not brick an old binary
- leave the keys of an unrecognised section alone; that section is already
  reported whole, and listing its keys would charge one mistake twice

Co-Authored-By: Vulcan <chess7th@pm.me>
No behaviour change. Groundwork for naming the config file in an error:
the resolved path was thrown away, because the function that looks like it
returns a path returns the file contents.

- split read_config_file() out of retrieve_config_file_path(), and add a
  get_ini_file_at() that takes the path instead of resolving it again
- keep both public entry points and their behaviour exactly as they were
- store the resolved path on GlobalVars, left unset when a caller hands in
  a config directly

Co-Authored-By: Vulcan <chess7th@pm.me>
The panic named a key and a source line, never the config file to edit, so
diagnosing a rename meant reading the source.

- add require_key(), replacing four scattered expects with one fatal path
  that names the resolved config file and the [section] key
- name the unrecognised keys of that same section in the same message: a
  misspelt commit_paths is the usual reason commit_path is missing
- treat a blank value as missing; commit_path= passed the presence check
  and silently journalled into the vault root at exit 0
- withdraw the empty-root_path_dir contract a test used to bless, where an
  empty vault root resolved to / and the run carried on

Co-Authored-By: Vulcan <chess7th@pm.me>
[templates] commit_datetime was read from the config and required on pain
of a fatal error, then never consumed: the TIME column was hardcoded to
%H:%M:%S. A typo in that key could abort a run over a value nothing read.

- thread the configured format from main through run_commit_saver and
  append_entry_to_diary to the row builder
- cover it: the configured format reaches the row and the hardcoded one no
  longer wins

No output change for the live config, whose value is already %H:%M:%S.

Co-Authored-By: Vulcan <chess7th@pm.me>
The section fix had a real-binary test asserting stderr; the key work had
no twin, which is how a silent key stayed silent.

- assert an unrecognised key is named on stderr by the actual binary, and
  that a fully known config still says nothing
- assert a missing required key names the config file, the [section] key
  and the typo that explains it
- key the silent-case positive control on the repo-discovery failure
  rather than on the word panicked, which any config fault would satisfy

Co-Authored-By: Vulcan <chess7th@pm.me>
A unit test cannot show what this tool actually does wrong: every failure
it has shipped was a config fault at the hook boundary, where the binary
runs with no RUST_LOG and the only thing a human sees is stderr.

- drive a real git commit through a real post-commit hook against each
  config fault, in a throwaway repo and vault with an explicit hooksPath
- report the commit exit status, the stderr, and what was journalled,
  which is exactly the evidence the last two fixes turned on

Co-Authored-By: Vulcan <chess7th@pm.me>
- state the key rules beside the section rules they mirror: unknown keys
  are named on stderr, required keys stay fatal, a blank value counts as
  missing
- show the fatal message, so the shape of it is the documented contract
- say plainly that no config fault can cost a commit, since the tool runs
  post-commit and git ignores that exit status
- point at the hook gate script for checking the behaviour by hand

Co-Authored-By: Vulcan <chess7th@pm.me>
The prose still described the behaviour from before 4.17.0: matching on the
working-directory name. It has matched the canonical name from the origin
remote since, which is what makes one entry cover every worktree.

Co-Authored-By: Vulcan <chess7th@pm.me>
- pin both empty cases of the per-section key lookup: an unknown section,
  and a known section the config does not carry
- exercise get_ini_file() through the env var, which nothing covered once
  set_all() stopped calling it

Co-Authored-By: Vulcan <chess7th@pm.me>
- a blank value defeated the presence check: commit_path= exited 0 and
  journalled into the vault root
- commit_datetime was required and never read, so a typo could abort a run
  over a value nothing consumed; wiring it up was Franci call
- add both to the acceptance scenarios

Co-Authored-By: Vulcan <chess7th@pm.me>
Usage said the pre-commit hook invokes the saver. It has always been the
post-commit stage, and the difference matters: a post-commit hook cannot
affect the commit, which is what makes a fatal config error safe.

Co-Authored-By: Vulcan <chess7th@pm.me>
- four keys are required, not five: [exclude] repos is optional, like its
  own section says two paragraphs above
- the example comment still described exclusion by working-directory name,
  which the prose beneath it had already corrected
- list the blank-value case the hook gate supports

Co-Authored-By: Vulcan <chess7th@pm.me>
Honouring commit_datetime made a bad value reachable, and it surfaced from
inside the writer as "a formatting trait implementation returned an error",
naming neither the file nor the key - after an empty diary file had already
been created. commit_date_path had the same hole before this lane.

- check both format keys where the rest of the config is checked, with the
  same message shape: file, [section] key, and the value that fails
- the run now stops before writing anything at all

Co-Authored-By: Vulcan <chess7th@pm.me>
The four setters still documented only a missing key as fatal, while their
shared reader also rejects a blank value and an unrenderable format.

Co-Authored-By: Vulcan <chess7th@pm.me>
The new format check landed between require_key and its documentation, so
the most safety-critical function in the config path ended up undocumented
while the new one inherited two Panics sections, the first describing the
wrong function.

- move the function below require_key, where its own docs belong
- cover the second call site: reverting the date-path guard alone left the
  whole suite green
- say format rather than time format, since one of the two keys is a path
  template

Co-Authored-By: Vulcan <chess7th@pm.me>
The lane gate is a real commit through a real hook; the format check had
only unit tests behind it.

- add a bad-format case to the hook gate script
- assert at the binary boundary that the message names file, key and value,
  and that the vault is never created

Co-Authored-By: Vulcan <chess7th@pm.me>
Missing and blank values were covered; a format chrono cannot render was
not, though it is fatal the same way.

Co-Authored-By: Vulcan <chess7th@pm.me>
It ran in a bare temp directory, so the binary always died in repository
discovery before it could write - the assertion passed even with the format
check deleted, while reading like a guard.

- run it inside a real git repository, so the run reaches the point where
  it would create the empty diary file the check exists to prevent
- add the control it needed: a renderable format in the same setup must
  journal, otherwise wrote-nothing proves nothing

Verified by mutation: neutering the check now fails this test.

Co-Authored-By: Vulcan <chess7th@pm.me>
Five tests in this module write RUSTY_COMMIT_SAVER_CONFIG and two read it
back. Under cargo test they share one process and race: a reader can see the
value another test just set, and the run fails on a path it never wrote.
Nextest, which the gate uses, gives each test its own process and hides it.

- take one lock around every test that touches the variable
- ignore poisoning: several of these panic deliberately, which is no reason
  to fail the rest

Measured: 100 threaded runs, 0 failures, against roughly 3 in 100 before.

Co-Authored-By: Vulcan <chess7th@pm.me>
It asserts the run reaches the point of creating the vault, not that a row
lands in the file; the row is covered in vim_commit.

Co-Authored-By: Vulcan <chess7th@pm.me>
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.31818% with 20 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/config.rs 94.51% 18 Missing ⚠️
src/vim_commit.rs 91.66% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@chess-seventh
chess-seventh merged commit 8c2af0f into master Jul 30, 2026
6 checks passed
@chess-seventh
chess-seventh deleted the feat/l76-key-skew branch July 30, 2026 23:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant