β¨ A Rust flake to automatically log al Git commits into Obsidian. β¨
Rusty Commit Saver captures each commitβs:
- Timestamp
- Commit message
- Repository URL
- Branch name
- Commit hash
and appends it to a dated diary entry in your Wiki directory.
Table of Contents:
- π Features
- π¦ Installation
- π Usage
- π οΈ Configuration
- π Roadmap & Improvements
- π Contributing
- Automatic diary entry creation with YAML frontmatter and table header
- Timestamped commit rows formatted for Obsidian
- Customizable storage path under
π Diaries/0. Commits/YYYY/MM-MMMM/ - A
git logbackstop (--reconcile) that journals commits made on a machine with no vault mounted, so a row is late rather than lost β for the historyHEADreaches - Preconfigured hooks (via Nix + pre-commit) to ensure code quality
-
Clone the repository
git clone https://github.com/chess-seventh/rusty-commit-saver.git cd rusty-commit-saver -
Enter the Nix development shell
devenv shell
-
Build the project
cargo build --release
-
(Optional) Install the binary to your PATH
cargo install --path .
devenv shell -- cargo test # the whole suite
devenv shell -- cargo clippy --all-targets
devenv shell -- pre-check # linters + tests + build
devenv.nixneeds nothing outside the repository. It enriches the shell with the shared modules from thedevenv_sharedcheckout when the machine has one β at~/src/claude-src/repos/devenv_shared, or at the older~/devenv_sharedβ and evaluates fine without it, which is what a fresh box and a CI runner get. Everything the commands above need (the Rust toolchain,cargo-nextest/cargo-shear/cargo-llvm-cov,treefmtand its formatters) is declared indevenv.nixitself, so this gate is reachable anywhere.The repository's own flake is self-contained too, so
nix develop --command cargo testalso works on any machine with Nix. Note that itsrustfmtdefaults to a different style edition β format withcargo fmt -- --style-edition=2024there, or it will reflow files it should leave alone.
devenv.yaml pins the devenv module input to the release whose module version
equals the installed devenv CLI. Left unpinned, that input follows
cachix/devenv's default branch, so devenv update locks modules newer than
the CLI, and dotenv.enable = true β which this repository sets β then fails
at evaluation with:
The dotenv integration requires the C-Nix devenv CLI. It is not
available through the flake integration or another standalone Nix evaluation.
The failure lands on the next direnv load rather than on the update, so it
does not look like the update caused it. Recovery is git restore devenv.lock.
The tag name is not the module version. devenv compares devenv version
against the pinned module's src/modules/latest-version, and cachix's tags run
one release ahead of it β tag v2.2.2 ships module 2.2.1, tag v2.2.1 ships
module 2.2.0. So pin the tag whose latest-version equals the CLI, not
the tag of the same name:
| installed CLI | pin | module it locks |
|---|---|---|
2.2.1 (the fleet today) |
ref=v2.2.2 |
2.2.1 |
Bump the pin only together with the fleet's devenv CLI, and never above it.
Equal is the only value that both keeps the environment and silences the notice.
A module behind the CLI still evaluates, but devenv keeps printing "run
devenv update to sync" β which is the prompt that caused this defect. A module
ahead of the CLI leaves the repository with no environment at all.
Simply commit as usual. The hooks will:
- Run linters (
clippy,rustfmt, etc.) inside the Nix shell β pre-commit - Invoke Rusty Commit Saver to log the commit β post-commit, once the commit exists, which is why nothing this tool does can cost you a commit
If you prefer manual invocation:
rusty-commit-saverYour commit will be appended to, where Obsidian should be:
~/Documents/Wiki/π
Diaries/0. Commits/YYYY/MM-MMMM/YYYY-MM-DD.md
The hook can only journal on a machine that mounts the vault. Everywhere else the row is lost, and once the config is present on those machines it is lost silently β a journal that quietly stops looks exactly like a quiet week.
--reconcile is the other half: it reads the history each checkout's HEAD
can reach and appends whatever row the day note is missing. It needs no network
and no broker, so it works on the machine where the vault actually lives,
whenever you run it.
HEAD-reachable is narrower than "the repository", and the difference bites. Commits sitting on a branch that is not the checkout's currentHEADare not journalled, and if that branch is later squash-merged they never become reachable at all. If you work in git worktrees, point--reconcileat each worktree rather than only at the main clone.
# everything each of these checkouts can reach from its own HEAD
rusty-commit-saver --reconcile ~/src/one --reconcile ~/src/two
# just the recent past, for a scheduled run
rusty-commit-saver --reconcile ~/src/one --since 2026-08-01Each repository gets one line on stdout, so a scheduled run that appended nothing still says so:
rusty-commit-saver: one: 412 scanned, 3 appended, 409 already present
rusty-commit-saver: claude-src: excluded
What it will and will not do:
- It only appends. A row already in a note is never rewritten, reordered or reformatted. Your day notes are yours.
- A row goes in the note for the commit's own date, never today's β so a first backfill spreads across the months it actually happened in.
- A row the hook already wrote is recognised, by the commit hash in the last column, and not written twice. Running the pass a second time appends nothing and leaves every note byte-identical.
- The
[exclude]list applies here too. An excluded repository is not even walked. - A repository it cannot open is reported on stderr and skipped, so one broken clone does not stop the other repositories from catching up.
- It ignores the directory you started it in. Unlike the hook, it journals only the repositories you named.
--since takes a date as YYYY-MM-DD. A value it cannot read stops the run and
names both what it wanted and what it got, rather than quietly backfilling years
of history from a typo in a timer unit.
rust-toolchain.tomlpins Rust 1.89.0devenv.nixprovisions Rust, Clippy, rustfmt, and Git hooks.pre-commit-config.yamldefines all pre-commit checkstreefmt.tomlconfigurestreefmtand formatters
Runtime settings live in an INI file at
~/.config/rusty-commit-saver/rusty-commit-saver.ini:
[obsidian]
root_path_dir = ~/Documents/Obsidian
commit_path = Diaries/Commits
[templates]
commit_date_path = %Y/%m-%B/%F.md
commit_datetime = %H:%M:%S
# Optional: repositories to skip, by canonical repo name (comma-separated).
# A commit made in one of these repos writes nothing to the diary.
[exclude]
repos = claude-srcThe [exclude] section is optional. Each entry is matched, case-sensitively,
against the committing repository's canonical name β taken from its origin
remote URL (β¦/claude-src.git β claude-src), falling back to the
working-directory name for a repo with no usable origin. Because the origin is
the same from every checkout, one entry covers the main clone and every git
worktree of that repo, from any subdirectory.
[obsidian] and [templates] are required; a config missing either one is
fatal. Any other section is ignored, with a line on stderr naming it, never
fatal. One INI file is shared by every checkout on the machine, so a section
written for a newer release must not break a binary that predates it β which is
exactly what adding [exclude] did to every checkout older than 4.17.0.
The stderr line matters: a misspelt section ([excludes]) is ignored too, so
without it your exclusions would silently stop applying.
Keys work the same way, for the same reason:
-
A key this binary does not understand is ignored and named on stderr (
ignoring unrecognised config keys [templates] commit_datetimes). It used to be ignored in complete silence, so a typo applied nothing and said nothing. -
The four keys in
[obsidian]and[templates]are required, and so is a non-empty value for each βcommit_path =counts as missing. Without them there is no destination to write to, and a hook that quietly journals nothing looks exactly like a quiet day, so this one stays fatal. ([exclude] reposis optional, like its section.) -
The two
[templates]values must be formatschronocan actually render, and that is checked when the config is read. A bad specifier used to surface from inside the writer asa formatting trait implementation returned an error, naming nothing, after an empty diary file had already been created. -
The fatal message names the config file, the key and its section, plus any unrecognised key in that same section, since a misspelt
commit_pathsis the usual reasoncommit_pathis missing:rusty-commit-saver: /home/you/.config/rusty-commit-saver/rusty-commit-saver.ini: missing required key 'commit_path' in section [obsidian]; unrecognised in [obsidian]: commit_paths
None of this can cost you a commit: the tool runs as a post-commit hook, and git ignores that hook's exit status. A config fault costs you the diary entry and prints on stderr; the commit itself always stands.
tests/hook-gate.sh drives a real commit through a real post-commit hook, in a
throwaway repo and vault, and prints what a human would see:
cargo build
./tests/hook-gate.sh good # journals, says nothing
./tests/hook-gate.sh unknown-key # journals, names the key on stderr
./tests/hook-gate.sh missing-key # journals nothing, names file + key
./tests/hook-gate.sh blank-key # same, for a key with an empty value
./tests/hook-gate.sh bad-format # same, for a format chrono cannot render
./tests/hook-gate.sh unknown-section # journals, names the sectiondevenv installs this repository's own git hooks, and four of them are the
fleet gate β the checks every repository on the fleet shares (secret
scanning, gitlint, commitizen). They are reached from inside this
repository's own prek run rather than from a global git config value, which
is what L235 changed: a global core.hooksPath made prek refuse to install
this repository's hooks at all, so devenv test failed here and
devenv shell -- cmd still returned 0.
hooks/fleet-gate-hook is a tracked copy of the script the flake packages and
tests. The flake is private, so taking it as an input would put a deploy-key
wall in front of entering this shell on every box, CI included.
On a box with no fleet gate installed it says so on every commit, and never blocks:
fleet gate: NOT INSTALLED on this box - pre-commit ran this repo's hooks only
To see what a commit would be refused for, without making one:
cfg=$(devenv build git-hooks.configFile | grep -o '/nix/store/[^"]*')
devenv shell -- prek run --all-files -c "$cfg"To skip a single fleet hook in this repository only β --no-verify turns the
whole gate off, which is not the same thing:
git config hooks.fleetGate.skip <hook-id>
git config --unset hooks.fleetGate.skip # restore itβ This repository's other hooks are conditional on a path that can vanish
silently. sharedModules in devenv.nix is built from
builtins.getEnv "HOME" filtered by builtins.pathExists, so on a box without
~/src/claude-src/repos/devenv_shared checked out the list is empty and this
repository declares no hooks at all, with no error. The four fleet-gate entries
are declared in devenv.nix directly, so the gate does not depend on that
lookup succeeding.
β If git commit here starts failing with config file not found, the
shared .git/hooks holds prek shims pointing at a worktree that has been
reaped. Clear them once:
prek uninstall
devenv shell -- true # re-installs from this repo's own configThere are many enhancements planned:
- Configurable Obsidian path
- Configurable year/day/month on where to save the commit
- Interactive CLI flags and richer metadata (author, files changed)
- Improved error handling and user feedback
- Unit tests and CI pipeline for automated releases
Contributions welcome! Feel free to open issues or submit PRs.
- Fork the repo
- Create a feature branch
- Write tests and update
README.md - Submit a pull request
MIT Β© 2026 Chess7th