Skip to content

Commit on green: savepoint, and the squashing it makes necessary

Level: 201 · working knowledge

One line: savepoint ↗ watches your files, runs your test command, and makes a git commit on exactly one transition — failing to passing — so a known-good state is always one git reset away; the price is a pile of identical commits, which is what squashing removes before anyone sees them.

What it is

A small Rust CLI (v0.3.12 on crates.io). You give it a file extension to watch and a command to run:

savepoint -f rs -- cargo test
savepoint -f js -- yarn test

It then loops forever: run the command, report, wait for a file to change, repeat. When the command starts succeeding, it runs git commit for you with the message SAVEPOINT REACHED!.

Usage: savepoint [OPTIONS] --filetype <filetype> [COMMAND]...

  -f, --filetype <filetype>  Filename extension to watch (eg rs, js, py, java)
  -d, --dryrun               Don't run git commit when tests pass
  -c, --clear                Clear screen between executions
  -q, --quiet                Don't display test output

--dryrun is the flag to start with: same watching, same reporting, no commits, so you can see what it would have saved before you let it write to your history.

The one transition that matters

The whole tool is two states and four edges, and only one of the four does anything:

Verified output of commit_on_green.rs — regenerated by tools/run_examples.py, never hand-typed.

what you did                               tests      savepoint
------------------------------------------------------------------------------
write the first test and make it pass      pass       SAVEPOINT REACHED  <- commit
refactor, still green                      pass       still passing     (no commit)
start the risky change                     FAIL       Error!
try something                              FAIL       still failing
try something else                         FAIL       still failing
revert to the last savepoint and retry     pass       SAVEPOINT REACHED  <- commit
tidy up                                    pass       still passing     (no commit)

2 commits from 7 edits.
Note what did NOT commit: the second green run, and the fourth and
fifth failures. Only the EDGE from red to green saves, so a passing
suite you keep re-running does not bury you in commits.

Both say "SAVEPOINT REACHED!", which is why the next step is squashing.

That gating is the design. Committing on every green run would produce a commit per keystroke-plus-save; committing only when red becomes green produces a commit per thing you actually fixed.

Why bother: the revert path

The argument is not "commits are good." It is about which of two roads out of broken you can afford:

  • Keep debugging — unbounded. You do not know when it ends, and half an hour in you cannot remember what you changed.
  • Revert and retry — bounded, and usually fast. But only available if a known-good state exists and you can find it.

Most people take the first road because the second is not really on offer: the last commit was 90 minutes and four ideas ago, and git diff is 300 lines. savepoint's actual job is to keep the second road permanently open, so "throw this away and try again" is a real choice rather than a thing you say afterwards.

Is it "stash with GitHub"?

No — and the differences are the interesting part. It touches neither stash nor GitHub.

git stash savepoint
Triggered by you, manually your tests going green
Produces an entry on a side stack a real commit on your branch
Part of history no yes
Ordered narrative no — a pile yes — a sequence
Survives until you pop it, or forget it forever, until you rewrite it
Touches a remote never never

git stash is a shelf: you push work aside to do something else, then pop it back. It is temporary by design, it is not history, and stashes are famously easy to abandon and lose.

savepoint makes commits — permanent, ordered, on your branch, each one a state the tests certified. And nothing is pushed anywhere: savepoint runs git commit, never git push. Your remote sees nothing until you decide.

The metaphor in the name is the right one: it is a game autosave. Can't beat the boss? Load a previous save.

What squashing is, and why savepoint forces the question

Squashing is combining several commits into one before you share them.

You need it here because thirty commits all called SAVEPOINT REACHED! are worthless to a reader. They record when you typed, and a good history records what you decided. The slide's recipe is the standard one:

git reset --soft HEAD~3
git commit -m "FEAT: New user page"

Read git reset --soft HEAD~3 precisely, because the three modes differ in exactly one way each:

Mode Moves the branch Index (staging) Working tree
--soft yes kept kept
--mixed (default) yes reset kept
--hard yes reset destroyed

--soft HEAD~3 rewinds the branch pointer three commits and changes not one byte of your files. All the work from those three commits is sitting staged, ready to be committed again as one. That is why the pair of commands is safe: nothing is deleted, the same tree gets a better message.

--hard is the one that eats work. Same command shape, opposite outcome.

The rule that matters: only squash what you have not shared. Squashing rewrites history — the new commit has a different hash, so anyone who already pulled the old ones now has a divergent branch to untangle. Before a push it is free housekeeping; after a push it is something you inflict on colleagues. This library's own CONTRIBUTING ↗ is stricter still, and forbids history rewriting in a shared checkout outright, because a rebase there drops other people's commits into your working tree.

Two alternatives worth knowing: git rebase -i HEAD~10 gives you a per-commit editor (pick/squash/reword) when the run is not a clean suffix, and GitHub's Squash and merge button does the whole thing at merge time — which is why plenty of teams never squash locally at all.

Should you use it?

The habit, yes. The tool, probably not yet. Be clear about which you are adopting.

Arguments against reaching for it today: it is v0.3.12 with 163 downloads — genuinely new, not yet load-bearing anywhere. It commits with git commit -a, so every dirty tracked file rides along, including the scratch edit you did not mean to keep. And it only works if your suite is fast: a two-minute test run turns a save-on-write loop into a machine that is always busy and never current.

What you can have for free, right now:

  • --dryrun first, always, to see what it would have saved.
  • bacon already gives you the watch-and-report half with a nicer interface; savepoint adds only the commit.
  • The manual version costs nothing: when the tests go green and you are about to try something risky, git commit -am wip. savepoint is that habit automated, and the habit is where the value is.

Then squash before pushing, and the throwaway commits never existed as far as anyone else is concerned.

What is Nushell?

It appears here because savepoint was prototyped in it before the Rust rewrite — a good trajectory, and worth naming.

Nushell ↗ is a shell whose pipelines carry structured data instead of text. In bash, ls | ... hands the next command a block of characters that you then chew apart with awk, cut and sed, re-deriving the structure the first command already had. In Nu, ls yields a table with typed columns:

ls | where size > 1kb | sort-by modified | first 5

No parsing, and no quoting bugs, because nothing was ever flattened to text. It is written in Rust, works on Linux, macOS, BSD and Windows, and is popular — 40k stars, and nu on crates.io has ~449k downloads.

The one caveat before you switch: it is pre-1.0 (v0.115.1) and still ships breaking changes between releases. It is a genuinely nice scripting language and a moving target, which is exactly why a prototype graduating from Nu to Rust is a reasonable thing to do rather than a criticism of either.

If you are coming from another language

  • Python — the closest habit is committing before each risky refactor, and the closest tool is pytest-watch. Nothing in the Python world commits for you, and the reason savepoint can is that a compiled language gives a much sharper pass/fail signal.
  • ABAP — version handling lives in the transport system and there is no local commit at all, so the habit has no equivalent: you cannot cheaply snapshot a half-finished state. Git's cheap local commit is the capability being exploited here, and it is one of the genuinely new things to learn coming from a transport-based world.

See also

  • bacon — the watch loop without the commits, and where watchexec fits instead
  • cargo-nextest — making the suite fast enough for a save-on-write loop to work
  • Strict clippy lints — the other way to be told you broke something, earlier

Po polsku

Nazwa savepoint jest tu najlepszym wyjaśnieniem: to punkt zapisu, dokładnie ten z gier — nie pokonasz bossa, wczytujesz ostatni zapis i próbujesz inaczej. Narzędzie obserwuje pliki, uruchamia twoje polecenie testowe i robi git commit przy jednym jedynym przejściu: z czerwonego na zielone. Nie przy każdym zielonym przebiegu — inaczej dostawałbyś commit po każdym zapisaniu pliku — tylko w momencie, w którym coś faktycznie naprawiłeś. Sens tego jest praktyczny, nie estetyczny: z zepsutego stanu prowadzą dwie drogi, „debuguję dalej” (nie wiadomo, ile potrwa, a po pół godzinie już nie pamiętasz, co zmieniłeś) i „cofam się i próbuję od nowa” (krótka i przewidywalna). Druga droga jest dostępna tylko wtedy, gdy istnieje znany dobry stan i potrafisz go wskazać — i to jest cała praca, którą savepoint wykonuje.

Warto od razu rozbroić dwa nieporozumienia, bo oba pojawiają się przy pierwszym opisie narzędzia. To nie jest git stash: schowek to półka na chwilowo odłożoną pracę, poza historią, bez kolejności i notorycznie zapominana — a savepoint robi prawdziwe commity na twojej gałęzi, uporządkowane i trwałe. To nie ma też nic wspólnego z GitHubem: narzędzie wywołuje git commit, nigdy git push. Zdalne repozytorium nie widzi niczego, dopóki sam nie zdecydujesz.

Cena za to jest jedna: stos identycznych commitów SAVEPOINT REACHED!, które trzeba scalić w jeden przed pokazaniem komukolwiek (squashing). Standardowy przepis to git reset --soft HEAD~3, a potem jeden git commit z porządną wiadomością — i tu leży miejsce, w którym polskie fora najczęściej straszą. Trzy tryby git reset różnią się dokładnie jedną rzeczą każdy: --soft przesuwa wskaźnik gałęzi i nie zmienia ani jednego bajtu w twoich plikach (cała praca czeka w poczekalni, gotowa do ponownego zatwierdzenia), --mixed czyści poczekalnię, a --hard kasuje drzewo robocze. Ten sam kształt polecenia, przeciwny skutek. Druga reguła jest równie prosta i równie łatwo o nią potknąć: scalaj tylko to, czego jeszcze nie wypchnąłeś. Squash przepisuje historię i zmienia skróty commitów, więc przed push jest darmowym sprzątaniem, a po push — problemem, który fundujesz współpracownikom.

Na koniec uczciwie: przyjmij nawyk, niekoniecznie narzędzie. savepoint ma wersję 0.3.12 i 163 pobrania, commituje przez git commit -a (więc zabiera ze sobą każdy brudny śledzony plik), a przy wolnym zestawie testów zamienia pętlę w maszynę, która zawsze liczy i nigdy nie jest aktualna. Zacznij od --dryrun. Ręczna wersja nie kosztuje nic i daje niemal całą wartość: gdy testy przechodzą, a ty właśnie zabierasz się za coś ryzykownego, wpisz git commit -am wip.

Szukaj po polsku: squash commitów · przepisywanie historii w gicie · git reset soft mixed hard · git squash before push · savepoint namtao