gitomic records atomic commits while a git repository changes, then stamps a single message across the
whole batch when the work is done. A session of many small, individually recoverable steps collapses into one
authored intent — without flattening the intermediate history into a single commit.
It is deliberately narrow: it never touches credentials, never contacts a remote, and writes nothing but local
commits. Publishing stays an explicit, separate git push.
A session has two pieces of state, both inspectable with plain git:
- A ref
refs/gitomic/base/<branch>marking the commit HEAD sat on when the session began. - A background watcher process, identified by a pidfile under
<git-dir>/gitomic/<branch>/.
Recording happens on the branch you are standing on. gitomic init does not check anything out, and no gitomic
command moves HEAD or the work tree while a session is open.
gitomic init plants the base and forks the watcher. As the work tree settles after each burst of edits, the
watcher records one commit with an empty placeholder message — unless the settled paths are exactly the paths
recorded by the session's most recent commit, in which case the capture extends that commit instead (see
coalesce_same_file below). gitomic finish stops the watcher, collects every commit in base..HEAD, obtains
one message, and rebuilds the batch onto the base with that message applied to each commit (original tree and
authorship preserved, fresh committer). The branch ref is advanced with a compare-and-swap and the base marker
is removed.
Because the batch is unpublished, rewriting the messages is safe. The result is N commits carrying the same
message; each remains individually recoverable by hash and reflog. Enable numbering to append [i/N] so the
commits stay distinguishable in git log.
A commit that reaches base..HEAD without coming from the watcher — a pull that advanced the branch, a
manual commit, work from another client — is reported by gitomic status and keeps its own message through the
finalize; only the placeholders are restamped. Two cases stop the finalize instead, with the branch untouched
and the session preserved: a commit a remote-tracking branch already contains, since rewriting it would need a
force push to publish, and a merge commit, which a single-parent replay cannot carry. push_guard optionally
installs a pre-push hook that refuses to push a branch whose batch is still unfinalized.
Run any command from anywhere inside the repository; the repository is inferred from the working directory.
gitomic init # mark HEAD as the base and start watching (alias: start)
gitomic status # base, pending atomic-commit count, watcher state, log path
gitomic finish -m "message" # stop watching and stamp the message across the batch (alias: commit)
gitomic finish # same, but open $GIT_EDITOR for the message
gitomic stop # stop watching, keep the base and commits for later finish/resume
gitomic abort --force # discard the session: reset the branch to the base
gitomic drop <hash>... # delete individual unpublished commits (alias: rm)
gitomic cherry-pick [<hash>...] # bring commits from another branch, via a patch file (alias: pick)
gitomic restore [<path>...] # bring files from other branches into the work tree as they are there
gitomic pick --stash [<path>...] # the same, out of a stash; the stash keeps the file (alias: restore --stash)
gitomic restore -s -m <path>... # merge a stashed file into the work-tree copy one change at a time
gitomic restore -M [<path>...] # put modified or deleted files back to their staged state, in place
gitomic history <path> # the commits that changed one file, each with its change; Enter restores a version
Running init again when a base already exists but no watcher is live resumes that session rather than
starting a new one, so an accidental stop — or a reboot — is recoverable without losing recorded commits.
-m, --message <text>— use<text>and skip the editor.-n, --numbering— append[i/N]to each commit message this run.--no-numbering— do not append ordinals this run (overrides the config default).
gitomic drop <hash>... deletes individual commits that have not been pushed, using the short hashes printed
by the finish editor template, gitomic diff, or git log.
- Eligible commits are the open session's pending batch (
base..HEAD) or, when no session is open, any commit on the checked-out branch that no remote-tracking branch contains. A commit that is already in a remote-tracking branch, a root commit, and a merge commit are refused. - History is rewritten by re-applying the later commits onto the surviving parent, keeping each one's message and authorship. The work happens in the object database, so nothing on disk changes until every replay has succeeded.
- Dependencies are detected, not guessed at: a later commit that does not apply without a dropped change (for instance, one that edits a file the dropped commit created) aborts the whole operation and is named in the error. Pass its hash as well to drop it together with the commit it depends on.
- Work tree: files the dropped commits added are removed and files they changed are restored, as
git reset --keepdoes. Uncommitted edits to any other file are kept; an uncommitted edit to a file that must be rewritten aborts the operation with nothing changed. - Session: a live watcher is stopped, its final capture flushed, and the watcher restarted afterwards, so the session carries on. The base marker is untouched.
- Recovery: the full id of every dropped commit is printed. The objects remain until garbage collection,
so
git cherry-pick <full id>restores one. -n, --dry-runreports what would be dropped and whether every later commit still applies, and modifies nothing.
Requires git 2.38 or newer (git merge-tree --write-tree).
gitomic drop with no hash opens a two-pane picker over the same eligible commits: the list on the left
(newest first, as in git log), the highlighted commit's header, stat and patch on the right. Marked
commits are shown in red with [x].
| pane | key | action |
|---|---|---|
| list | j k (or arrows) |
move; g / G jump to first / last |
| list | space |
mark or unmark the commit |
| list | l (or Right) |
move to the diff pane |
| list | Enter |
submit the marked commits; a y/n prompt follows |
| list | q, Esc |
quit (asks first when commits are marked) |
| diff | j k |
scroll up and down, stopping at the ends |
| diff | h l |
scroll left and right, stopping at the edges |
| diff | Enter, n |
next commit down the list |
| diff | Shift-Enter, N |
next commit up the list |
| diff | space |
mark or unmark the commit being viewed |
| diff | h at the left edge, Esc |
back to the list pane |
| diff | g G, Ctrl-d Ctrl-u |
top / bottom, half a page |
- Pressing
Enteron the list first runs the same dependency check asdrop. A selection that cannot be dropped (a later commit depends on a marked one) is reported on the bottom line and stays editable; they/nprompt appears only for a selection that can be dropped. - Only
yandnanswer the prompt.Enter,spaceand every other key are ignored, so an accidental Return cannot confirm anything.nandEsccancel and keep the marks. - Leaving the diff pane with
hrequires a press that is not part of a run ofhpresses that was still scrolling, so holdinghto reach the left edge stops there rather than jumping to the list. Shift-Enteris delivered only by terminals that support the kitty keyboard protocol (kitty, foot, WezTerm, Ghostty and others); elsewhere it arrives as a plainEnter, andNdoes the same job.-n/--dry-runapplies to the picker as well: the confirmation is shown, then nothing is modified.- Needs a terminal on stdin and stdout; otherwise the command stops with an error rather than waiting.
gitomic cherry-pick brings commits from another branch onto the checked-out one. The chosen commits are
replayed onto the current HEAD in the object database, the result is written as a patch file, and the patch
is applied to the work tree. History is not rewritten, so the operation is safe with unpushed commits and with
a live session.
- Without a session the change is left in the work tree, uncommitted (the effect of
git cherry-pick -n). - With a session (live or stopped) it is recorded as one atomic commit with the usual empty placeholder
message, which
finishstamps like any other step. Only the patch's own paths are committed; other staged changes stay out. A live watcher is stopped for the duration, as fordrop, and restarted afterwards. HEADmoved after the selection (the watcher committed, or a commit was made by hand): the patch is recomputed against the newHEADbefore it is applied, reusing every conflict decision whose conflict recurs unchanged. A conflict that was not decided stops the operation before anything is modified.- Local edits: the patch is checked against the work tree first. An uncommitted edit that overlaps a file the patch touches refuses the pick with nothing modified.
- Patch files are kept in
.git/gitomic-picks/. Each starts with a# gitomic-patch 1header naming its base, its source commits and the conflict decisions taken;git applyignores it, and it is what allows the patch to be recomputed against another base. A pick is undone withgit apply -R <patch file>(orgitomic droponce recorded). - Merge commits are not offered, and commits whose change
HEADalready holds under another id are hidden. - Options:
--from <branch>lists that branch first;-n/--dry-runreports the patch and applies nothing;-p/--patch-onlywrites the patch file and stops. With commits named on the command line (replayed in the order given) no terminal is needed, and any conflict is refused.
gitomic cherry-pick with no commit opens the same two-pane screen as the drop picker, with the same
vim-style keys, space to mark and the same rule that only y/n answer a prompt. What differs:
Tabopens an overlay listing the other local and remote-tracking branches, most recently updated first.j/kmove (PgUp/PgDna page),h/lscroll a name wider than the box,Enteruses the branch (marks are cleared),Esccloses it. Without--fromthe overlay is the first thing shown.- The right pane shows what each commit would change relative to the
HEADthat was current when the screen opened, not the commit's own diff. A commit that would conflict is announced above its diff. - A commit that would change nothing on
HEAD(its change is already there in another form) is drawn in gray and skipped byj/k,g/Gand, in the diff pane,n/N. Whether a commit applies is worked out for the commits near the selection while the screen is idle, and on demand when movement reaches one; a commit that conflicts counts as applying. When no later commit applies, the selection stays put and the status line says so. - The diff of a commit loads once the selection has rested on it for 150 ms, so holding
jorkdoes not run a merge for every commit passed. A diff already loaded is shown at once. - The screen opens, and a newly chosen branch is shown, with the selection marker outside the list: no commit
is highlighted and no diff is loaded.
j(orDown) enters from the top and lands on the first commit that applies;k(orUp) enters from the bottom.gandGalso leave this state. While it lasts, the two ends of the list are classified in the background, andspace,Randldo nothing. - A commit that changes a file larger than
cherry_size_limit_mb(32 MiB by default) is not merged ontoHEADand its files are not read: the preview lists the large files with their sizes and shows the commit as it was made, and the commit is never grayed, since that would need the merge.Doffers to read the files and show the change relative toHEAD; onlyyconfirms. Merging reads a large file on both sides, which is what makes the screen unusable on slow hardware when successive commits modify the same image. The text of one preview is also capped at 8 MiB, after which git is stopped and the cut is announced. - The git work runs on two background threads, so keys and drawing never wait for it. One loads the diff of the selected commit; when the selection moves on, its git process is killed and only the newest request is answered. The other works out which commits to gray, nearest the selection first, at a lower processor priority; it does not start a check while a diff is being loaded, and abandons one in progress when a diff is requested, asking again afterwards.
Rfollows one file. It marks the highlighted commit and every older commit of the source branch that changes the same file (the commitsHEADlacks), each restricted to that file, so that replaying them brings the file to its state on the source branch at the highlighted commit. When the highlighted commit changes several files, a small overlay asks which one to follow. A commit in the chain that also changes other files (one made withgit addrather than by gitomic) is applied for the followed file only; its other files are left out, and the status line counts such commits. Restricted marks show as[f](plain space marks stay[x]) and the diff title names the file; the preview shows the restricted form. Commits above the cursor keep their marks, and pressingRagain on a fully marked chain clears it. Renames are not followed (a file is tracked under the name it has in each commit), and merge commits are not in the list, so changes that reached the branch only through a merge are not part of a chain.- On the command line the same restriction is
--only <path>: every named commit is applied for that file only, and a commit that does not change it is refused. Enteron the list replays the marked commits, oldest first. A clean result goes straight to they/nprompt, which names the number of commits and the size of the change.- Conflicts open a decision screen: the conflicted files on the left, one entry per conflict hunk, and the
hunk on the right with a few lines of context and both sides one above the other.
akeeps the tree copy,btakes the picked commit's version,ckeeps both (tree copy first),uundoes,j/kmove between conflicts andEntercontinues once every one is decided. A file that cannot be split into hunks (binary content, or one side deleted the file) is decided as a whole witha/b. A later commit in the selection that conflicts opens the screen again. When the selection was made withR,Xabandons the replay of that file's history and restores the file whole from the newest commit marked for it (its content on the source branch, or absent if the commit lacks it); the confirmation prompt names the restored files. - Conflicts with no A/B meaning (a file renamed differently on both sides, a file/directory clash) are
reported and the selection is refused;
git cherry-pickhandles those.
gitomic restore brings files from other branches into the work tree exactly as they are at the tip of
those branches, overwriting the local copy. It is an overwrite, not a merge, so there is nothing to
decide; the operation runs on the same patch pipeline as cherry-pick (a patch file computed against
HEAD, checked, then applied). Every file is its own patch, so a file that cannot be applied (an
untracked file in the way, say) is reported and the others still go through.
- Outcome. With a session open (live or stopped), pending changes to tracked files are recorded first
as one atomic commit, then each restored file is recorded as one more, so the overwritten state is
always a commit and
gitomic dropundoes any of them. A live watcher is paused meanwhile. Without a session the change is left in the work tree, uncommitted, and a local edit to a file involved refuses the restore;git apply -R <patch file>undoes it. Untracked files in the way always refuse it. - Command line.
gitomic restore [--from <rev>] [-n] [-p] <path>.... Paths are relative to the current directory.--fromtakes a branch, tag or commit; without it the local branches must agree about each file, and a disagreement is refused with the branches listed.-n/--dry-runreports,-p/--patch-onlywrites the patch file and stops. - Merge (
-m/--merge, with paths, needs a terminal). Instead of overwriting the work-tree copy, the file is compared with the source's copy (-sfor the newest stash, or--fromfor a stash, branch or commit) line by line, and each separate change is put as a question on the conflict-resolution screen:akeeps the work-tree lines,btakes the source's,ckeeps both,uwithdraws a decision,j/kmove between changes and[/]between files,Enterwrites once every change is decided andqleaves with nothing written. An insertion in the source has an empty A side and a deletion an empty B side. Binary files, files absent from the work tree, and files identical to the source are reported rather than merged. Recording is as for restore: with a session open, pending edits are captured and each merged file is one commit (gitomic dropundoes it); otherwise the result is left uncommitted. The stash is only read.-nlists the number of changes per file without writing. The comparison is exact by lines, so a region changed in both the stash and later work appears as one change with both versions to choose from. Files whose differing region exceeds about four million line pairs are offered as a single change. - Interactive (no path, needs a terminal; also
Ffrom the cherry-pick screen,Esc/qreturns). Files on the left, what restoring the highlighted file would change on HEAD on the right (loaded on a worker thread after the selection has rested for 150 ms, as in the cherry-pick screen).Tabchooses the branches (local ones at first; space selects,aall/none,vinverts, so onevswaps the default branches for the stashes)./filters by name: every word must occur, case ignored, and a word with*or?is a glob matched against the file name (against the whole path when it contains a/), so*.imgandboot/*work;Escclears.Bshows only binary files.spacemarks and moves to the next file, so a run of presses marks a run of files. A file whose content differs between the selected branches is starred and asks which branch to take it from (vdoes the same on demand). Only files that differ from HEAD are listed (a file identical to HEAD has nothing to restore, and leaving it out keeps the memory and time to open the screen proportional to the change, not to the size of the tree); files HEAD lacks are cyan. A path named on the command line that already matches is reported and left alone.Hopens the history of the highlighted file: every commit on the selected branches that changed it (short id, date, branch, subject), read on a worker thread the first time and kept; the right pane shows each version against HEAD as the cursor moves andEntermarks that version.Dalso lists files that were deleted in the history of the selected branches and are gone from HEAD and from those tips, marked(deleted), each restorable at the version its deleting commit's parent held; the scan runs on a worker thread, stops when the branches change, and is kept while the mode is toggled. A historic mark shows as<- branch@commit.--from <commit>on the command line reaches any commit the same way.Plists the patch files kept in.git/gitomic-picks(newest first, with what each does and its text on the right);spacemarks,ddeletes the marked ones, or the highlighted one when none is marked, after ay/n.Enterasks for confirmation, answered byy/nonly. - Stashes. Each stash is listed after the branches in the
Taboverlay asstash@{N}with its message, unselected unless asked for. A stash offers only what it changed itself: the paths in which its work-tree state differs from the commit it was made on, plus the files of an untracked-files commit (stash push -u), each compared with HEAD, so the commits HEAD has gained since do not crowd the list. A file is taken whole, as the stash holds it; the stash is read and never changed, so the entry stays where it was.Sin the cherry-pick screen,gitomic pick --stashandgitomic restore --stashopen this screen with every stash selected. With paths,--stashtakes them fromstash@{0}, or from the stash named by--from, writtenstash@{N}or justN(gitomic pick --stash --from 1 src/main.rs). History (H) and the deleted-file scan (D) cover branches only; a stash has no history of its own. Files overcherry_size_limit_mbare restored but not previewed, as for branches. - Modified files. The
(modified)entry of theTaboverlay (shown only while some tracked file has edits that are not staged, i.e.git statuslists it under "Changes not staged" as modified; unselected unless asked for) offers those files at their staged state, which is whatgit restore <path>returns them to: a file with nothing staged goes back to HEAD, one with staged work keeps it. The right pane shows the work tree turning back into that state. It is a selective reset of the work tree: pick the files to put back and leave the others as they are (after a build has rewritten tracked files, say, and a few of them are to be swapped by hand before the next one).gitomic restore --modified [<path>...](-m) opens this screen with only that entry selected, or, with paths, restores those files; a path with no unstaged edit is refused. The edits are discarded in place: no commit is made, no session is needed, no patch file is written, and nothing records what was there, so they cannot be brought back. The screen'sy/nis the only confirmation; the command line has none, as withgit restore. Files deleted in the work tree are listed too and come back; untracked files and files whose only change is staged are not listed. The modified files cannot be combined with branches or stashes: choosing(modified)in theTaboverlay deselects everything else, choosing anything else deselects it, anda(all) leaves it out. Leaving the overlay with it newly chosen asksy/nbefore the file list is shown (not when it was already the selection, and not forrestore -M, which was asked for on the command line).-non the command line lists what would be discarded;-pis refused, since there is no patch.--modifiedcannot be combined with--stashor--from. History (H) and the deleted-file scan (D) do not cover it. - Keys.
PgUp/PgDnmove a page in the file list (and scroll a page in the diff pane when it has the focus); in every overlayh/l(or the arrow keys) scroll the rows sideways, which matters for long branch and stash names, the[x]marker staying in place. The same keys work in thedropand cherry-pick screens. - Limits. Only the current state (tip) of each branch is offered. Renames are not followed: a file is
looked up under one path. Submodules are left out. Files over
cherry_size_limit_mbare restored but not previewed.
gitomic history <path> lists the commits of the checked-out branch that changed one file, newest first, and
shows what each did to it. Enter restores the highlighted version.
- The list is the same reading as
Hin the restore screen (git logrestricted to the path), applied to the checked-out branch instead of the others, and it does not require the file to differ from HEAD. A commit that deleted the file is not listed, since it left nothing to look at; the version before it is, so a file that is gone can still be recovered. The atomic commits of an open session are listed like any other, their placeholder message showing as(no message)untilfinish. At most 300 commits are read, and the title shows a+when older ones were left out. Renames are not followed. - The right pane shows what the highlighted commit did to the file: its header and message and its diff
restricted to the path.
vswitches it to what restoring that version would change on HEAD (the text the restore screen shows); the newest version then says that restoring it changes nothing. - Keys.
j/kmove (PgUp/PgDna page,g/Gthe ends),lopens the diff pane (j/k,h/l,Ctrl-d/Ctrl-uandPgUp/PgDnscroll it,n/Nstep to the older and newer commit,Escreturns),Enterorrrestore after ay/nthat onlyyandnanswer,qquits. - Restoring takes the file whole as it was after that commit and goes through the same pipeline as
restore: with a session open (live or stopped) the pending changes are recorded first and the restore is one atomic commit thatgitomic dropundoes; otherwise the change is left uncommitted and a local edit to the file refuses it.-n/--dry-runreports and-p/--patch-onlywrites the patch file and stops. - No terminal. The command prints one line per commit (short id, date, subject), which is also what scripts
can read;
gitomic restore --from <id> <path>takes any of those ids. - The path is relative to the current directory, and exactly one file is taken.
${XDG_CONFIG_HOME:-~/.config}/gitomic/gitomic.cfg, a flat key = value file. Every key has a compiled-in
default, so the file is optional. See gitomic.cfg.example.
| key | default | meaning |
|---|---|---|
debounce_ms |
1000 |
Quiescence window; a burst of edits commits once this long has passed. |
finalize_numbering |
false |
Append [i/N] to each finalized message. |
stage |
observed |
Staging breadth: observed (only the paths the watcher saw change, new/renamed/copy-over included), tracked (git add -u), or all (git add -A). |
coalesce_same_file |
true |
Extend the prior atomic commit instead of starting a new one when a capture's paths exactly match it. |
cherry_size_limit_mb |
32 |
Size in MiB above which the cherry-pick and restore screens do not read a changed file for its preview (see below). 0 turns the limit off. |
(guarantees is a very strong word)
- Staging respects
.gitignore— the watcher uses git's ownadd, and a cycle that stages nothing produces no commit. - Staging is scoped to what the watcher saw change — under the default
stage = observed, a capture stages only the paths that fired events this cycle, intersected with the paths git reports as changed. A file created by a patch, a file-manager rename (delete of the old name plus creation of the new), or a copy that overwrites a tracked file is captured, because the watcher observed it; an untracked file the watcher never touched is left alone rather than swept into the session.stage = trackedrestricts staging to already- tracked paths (git add -u), andstage = allstages every change including untracked files (git add -A). - Edits made while no watcher ran are swept in at start — when a watcher starts or resumes, once the watch
is armed it records the modifications and deletions of tracked files (
git add -u) as one atomic commit, so work done beforegitomic initor between astopand a resume is part of the session instead of waiting for its file to change again. Untracked files are not swept (they are captured once the watcher sees them change), and submodules are left out entirely: a submodule checked out at another commit than the recorded one stays a local edit, and a pointer the operator already staged makes the sweep decline rather than commit it. The same applies to the capture made before a restore. Nothing is done when the checked-out branch is not the session's or the tree is clean. - Consecutive captures of the same file(s) share one commit — when a debounce cycle's staged paths exactly
match the paths of the session's most recent atomic commit, the capture amends that commit rather than
starting a new one (
coalesce_same_file, on by default). Editing a different file, or returning to an earlier file after editing something else, still starts a fresh commit. Not applied toexec, whose capture is a deliberate, explicitly requested result and always stands on its own. - The watcher stands down during multi-step operations — an in-progress merge, rebase, cherry-pick, revert,
or bisect suspends auto-committing; a held
index.lockdefers to a later cycle. - A detached HEAD is refused at
init— finalize needs a branch ref to advance. - One session per branch, and the watcher follows its own branch — a watcher records only while its branch is the checked-out one, and resumes by itself when you switch back, with no re-init. Switching to a different branch suspends capture for that session rather than committing onto whatever is current.
- Events inside the git directory are ignored — committing writes to
.git, so those writes are filtered to prevent a feedback loop. - Hooks are bypassed — placeholder commits use
--no-verify, and the finalize rewrite usescommit-tree, which does not run hooks. Run hooks manually if a session depends on them.
The original design contemplated an always-running daemon watching several configured directories. That is out
of scope here in favour of a session-scoped, manually started watcher tied to the current repository, which
avoids recording commits during periods the maintainer did not intend to track. The watch_dir config key is
accepted and ignored, reserved for a future multi-repository daemon should one prove useful. The known cost of
the manual model — forgetting to init before working — is documented rather than solved.
Requires a Rust toolchain and the git binary at run time. Linux only (inotify).
cargo build --release
install -Dm755 target/release/gitomic ~/.local/bin/gitomic