Skip to content
benjamindainesPublic

About

Automatically track per-file-edit commits and push them all at once when you're done. Maximum repeatability. Minimum annoyance.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

gitomic

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.

Model

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.

flow chart

Usage

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, picks still in the unpushed tree, watcher state, log path (colored on a terminal; NO_COLOR disables)
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 [-a] [<path>...] # the commits that changed the files, each with its change; Enter restores a version
                                 # (-a: every branch, not only this one; no path: browse the tracked files first)
gitomic pick --undo [<hash>...]  # undo commits of this branch with a new "reverted <sha>" commit; conflicts are rejected

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.

finish options

  • -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).

drop

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 --keep does. 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-run reports 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).

Interactive picker

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 / search: type text, Enter runs it, Esc cancels
list n N next / previous commit matching the last search, wrapping at the ends
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
  • Search (/, then n/N) looks for the typed text, ignoring case, in the commit's subject and in its id. The list wraps, and the bottom line says so. Marks are kept. In the diff pane / also starts a search, while n/N keep stepping through the commits there. The cherry-pick screen has the same keys.
  • Pressing Enter on the list first runs the same dependency check as drop. A selection that cannot be dropped (a later commit depends on a marked one) is reported on the bottom line and stays editable; the y/n prompt appears only for a selection that can be dropped.
  • Only y and n answer the prompt. Enter, space and every other key are ignored, so an accidental Return cannot confirm anything. n and Esc cancel and keep the marks.
  • Leaving the diff pane with h requires a press that is not part of a run of h presses that was still scrolling, so holding h to reach the left edge stops there rather than jumping to the list.
  • Shift-Enter is delivered only by terminals that support the kitty keyboard protocol (kitty, foot, WezTerm, Ghostty and others); elsewhere it arrives as a plain Enter, and N does the same job.
  • -n/--dry-run applies 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.

cherry-pick

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 finish stamps 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 for drop, and restarted afterwards.
  • HEAD moved after the selection (the watcher committed, or a commit was made by hand): the patch is recomputed against the new HEAD before 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 1 header naming its base, its source commits and the conflict decisions taken; git apply ignores it, and it is what allows the patch to be recomputed against another base. A pick is undone with git apply -R <patch file> (or gitomic drop once recorded).
  • Merge commits are not offered, and commits whose change HEAD already holds under another id are hidden.
  • Options: --from <branch> lists that branch first; -n/--dry-run reports the patch and applies nothing; -p/--patch-only writes 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.

Interactive screen

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:

  • Tab opens an overlay listing the other local and remote-tracking branches, most recently updated first. j/k move (PgUp/PgDn a page), h/l scroll a name wider than the box, Enter uses the branch (marks are cleared), Esc closes it. Without --from the overlay is the first thing shown.
  • The right pane shows what each commit would change relative to the HEAD that 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 by j/k, g/G and, 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 j or k does 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 (or Down) enters from the top and lands on the first commit that applies; k (or Up) enters from the bottom. g and G also leave this state. While it lasts, the two ends of the list are classified in the background, and space, R and l do nothing.
  • A commit that changes a file larger than cherry_size_limit_mb (32 MiB by default) is not merged onto HEAD and 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. D offers to read the files and show the change relative to HEAD; only y confirms. 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.
  • A commit that an earlier pick already brought into the unpushed commits or the uncommitted work tree is drawn in orange (italic as well, so it also shows on a terminal without color). Git no longer offers such a commit once HEAD holds its change, so the screen lists the ones from the chosen branch ahead of the others; like a gray commit they are skipped by the movement keys. gitomic status summarises the same picks, see "Picks in the tree" below.
  • / searches the list, n/N repeat the search (see the drop picker). Like j and k, a search passes over the grayed commits that would change nothing on HEAD, so the selection only lands on a commit that can be picked; when every match is gray the status line says how many there were.
  • R follows one file. It marks the highlighted commit and every older commit of the source branch that changes the same file (the commits HEAD lacks), 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 with git add rather 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 pressing R again 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.
  • Enter on the list replays the marked commits, oldest first. A clean result goes straight to the y/n prompt, 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. a keeps the tree copy, b takes the picked commit's version, c keeps both (tree copy first), u undoes, j/k move between conflicts and Enter continues 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 with a/b. A later commit in the selection that conflicts opens the screen again. When the selection was made with R, X abandons 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-pick handles those.

restore

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 drop undoes 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. --from takes 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-run reports, -p/--patch-only writes 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 (-s for the newest stash, or --from for a stash, branch or commit) line by line, and each separate change is put as a question on the conflict-resolution screen: a keeps the work-tree lines, b takes the source's, c keeps both, u withdraws a decision, j/k move between changes and [/] between files, Enter writes once every change is decided and q leaves 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 drop undoes it); otherwise the result is left uncommitted. The stash is only read. -n lists 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 F from the cherry-pick screen, Esc/q returns). 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). Tab chooses the branches (local ones at first; space selects, a all/none, v inverts, so one v swaps 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 *.img and boot/* work; Esc clears. B shows only binary files. space marks 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 (v does 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. H opens 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 and Enter marks that version. D also 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. P lists the patch files kept in .git/gitomic-picks (newest first, with what each does and its text on the right); space marks, d deletes the marked ones, or the highlighted one when none is marked, after a y/n. Enter asks for confirmation, answered by y/n only.
  • Stashes. Each stash is listed after the branches in the Tab overlay as stash@{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. S in the cherry-pick screen, gitomic pick --stash and gitomic restore --stash open this screen with every stash selected. With paths, --stash takes them from stash@{0}, or from the stash named by --from, written stash@{N} or just N (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 over cherry_size_limit_mb are restored but not previewed, as for branches.
  • Modified files. The (modified) entry of the Tab overlay (shown only while some tracked file has edits that are not staged, i.e. git status lists it under "Changes not staged" as modified; unselected unless asked for) offers those files at their staged state, which is what git 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's y/n is the only confirmation; the command line has none, as with git 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 the Tab overlay deselects everything else, choosing anything else deselects it, and a (all) leaves it out. Leaving the overlay with it newly chosen asks y/n before the file list is shown (not when it was already the selection, and not for restore -M, which was asked for on the command line). -n on the command line lists what would be discarded; -p is refused, since there is no patch. --modified cannot be combined with --stash or --from. History (H) and the deleted-file scan (D) do not cover it.
  • Keys. PgUp/PgDn move a page in the file list (and scroll a page in the diff pane when it has the focus); in every overlay h/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 the drop and 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_mb are restored but not previewed.

history

gitomic history [-a] [<path>...] lists the commits that changed the named files, newest first, shows what each did to them, and brings a version back. It is the view for finding out what touched a file that no longer builds: with -a it covers the other branches too, so a commit counts whether or not it was ever applied to the checked-out branch.

With no path, gitomic history first opens a browser over the files tracked at HEAD (/ filters by substring, Enter opens a file, Esc in a history comes back to the browser). In the list, J/K and Ctrl-d/Ctrl-u scroll the right pane without moving focus.

  • The list is the same reading as H in the restore screen (git log restricted to the path), 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) until finish. At most 300 commits are read per branch and file, and the title shows a + when older ones were left out. Renames are not followed. Commits are ordered by commit time, so commits made in the same second may appear in either order.
  • All branches. -a/--all, or a on the screen, reads every other local and remote-tracking branch after HEAD. A leading * marks a commit that the checked-out branch holds; the others are drawn in yellow, and the right pane's title says on HEAD or not on HEAD, on <branch>. A commit reachable from several branches is attributed to HEAD when HEAD holds it, otherwise to the first of the other branches that does (most recently updated first). a again returns to HEAD only.
  • Several files. Every positional word is a file. A commit that changed any of them is listed once, the right pane shows its change to just those files, and a restore takes all of them. A named file that no commit changed is an error, so a mistyped name is not left out silently.
  • Filter. / edits a substring, ignoring case, that narrows the list by commit id, subject or branch name. Enter keeps it, Esc clears it (and, once it is cleared, leaves the screen as usual).
  • The right pane shows what the highlighted commit did to the files: its header and message and its diff restricted to the paths. v switches 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/k move (PgUp/PgDn a page, g/G the ends), l opens the diff pane (j/k, h/l, Ctrl-d/Ctrl-u and PgUp/PgDn scroll it, n/N step to the older and newer commit, Esc returns), / filters, a toggles the branches, u undoes, Enter or r restore after a y/n that only y and n answer, q quits.
  • Restoring takes each file whole as it was in the highlighted commit (a file the commit does not contain is skipped and named) and goes through the same pipeline as restore: with a session open (live or stopped) the pending changes are recorded first and each restore is one atomic commit that gitomic drop undoes; otherwise the change is left uncommitted. -n/--dry-run reports and -p/--patch-only writes the patch file and stops. A version that only another branch holds can be restored like any other.
  • Local edits. Without a session, a restore of a file with uncommitted changes (staged, unstaged or untracked) would be refused after the screen had closed. The confirmation therefore asks first: s puts those files' edits into a stash (git stash push --include-untracked, message gitomic: edits to <files> before restoring <id>) and restores; d discards them (git restore --source=HEAD) and restores, and is not offered when a file is untracked; n cancels. Only the files being restored are touched, never the rest of the tree. The stash is an ordinary entry, so gitomic pick --stash or gitomic restore -s -m <path> can bring parts of it back. With a session open nothing is asked, since the pending edits are already recorded. A dry run or -p leaves the edits alone.
  • Undo. u on a commit that the checked-out branch holds undoes the whole commit with a new reverted <sha> commit, the same operation as gitomic pick --undo <sha> (see below): history is not rewritten, a conflict rejects the undo and changes nothing, and a session records it as an atomic commit that gitomic drop undoes in turn. A commit that is not on HEAD has nothing to undo, and merge and root commits are refused.
  • No terminal. The command prints one line per commit (short id, date, subject; with --all the source, HEAD or a branch name, follows the date), which is also what scripts can read; gitomic restore --from <id> <path> takes any of those ids.
  • Paths are relative to the current directory.

Finding what broke a build

A project stops building and two or three files are implicated. gitomic history -a path/a path/b lists every commit that touched them on any branch, * marking the ones already on the checked-out branch. v shows what restoring each would change on HEAD, so the commit that last had the files in a working state is easy to spot. From there: Enter brings the files back as they were in that commit; u undoes a single bad commit that HEAD holds; or restore only some of the change by hand with restore -s -m after stashing. Anything done while diagnosing is a normal commit or stash: with a session, gitomic drop <commit> removes it again, and gitomic pick --undo reverses a recorded one.

Picks in the tree

Every pick writes a patch file under .git/gitomic-picks, and the header of that file names the source commits (# pick <id> <subject>). The commits a session records for a pick carry neither the id nor the message, so gitomic status reads those headers back to answer "what did I pick earlier, and is it still here?":

    pending commits: 3
    picks:           1 pick from origin/feature-x (fc563fa0b1c2)
                     2 picks from main-old (0e5d4c3b2a19)
                     1 pick from origin/feature-x

There is one line per source branch and holding commit. A pick that a local commit holds (a session's atomic commit, or the result of finish) carries that commit's short id; a pick that exists only in the work tree has none. Committed lines come first, oldest commit first. A committed pick is not yet safe: it is lost if the branch is reset to an earlier commit, so it keeps its line until the commit is on a remote-tracking branch, and then the line goes away. With no session the same lines follow the "no active session" line, since a pick made without one is left uncommitted in the work tree. The lines are orange on a terminal with color, italic when NO_COLOR is set, and plain on a pipe or a dumb terminal.

A pick counts as present when at least 80% of the lines it changed are changed the same way between the last pushed state (the parent of the oldest commit that no remote-tracking branch contains, or the empty tree when nothing is pushed) and the work tree, untracked files included. The holding commit is the unpushed commit that contains the most of the pick's lines (the newest on a tie), and only if it contains at least half of them; at most the 500 newest unpushed commits are searched. The test does not need the patch to apply or reverse cleanly, so a pick edited afterwards is still found; a pick whose lines were all rewritten, reverted or pushed is not. The source is the first branch holding the commit (local before remote-tracking, most recently updated first), other than the checked-out one. Undo commits are not counted, and at most the 200 newest picks are examined. Patch files are never deleted; old ones simply stop matching.

Configuration

${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.

Behaviour and guarantees Intentions

(guarantees is a very strong word)

  • Staging respects .gitignore — the watcher uses git's own add, 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 = tracked restricts staging to already- tracked paths (git add -u), and stage = all stages every change including untracked files (git add -A).
  • A submodule pointer is committed only together with its .gitmodules entry — a directory that holds a repository of its own (a clone, or a repository moved into the tree) is not recorded by any capture: if the staging step picked it up as a bare pointer, that pointer is unstaged again and the directory stays untracked. A pointer the operator staged without its .gitmodules entry, as in the middle of git submodule add, is left staged and the capture stands down until the entry is staged as well.
  • 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 before gitomic init or between a stop and 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 to exec, 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.lock defers 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 uses commit-tree, which does not run hooks. Run hooks manually if a session depends on them.

Scope, and what is intentionally left out

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.

Building

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

undo (pick --undo)

gitomic pick --undo <hash>... undoes commits already on the checked-out branch by applying their reverse as a new change on top of HEAD; history is never rewritten, squashed or force-pushed. With no hash, the screen opens on the entry (undo: this branch) (also the first entry of the Tab overlay), listing the newest 150 commits of the branch. Without a session the undo is committed immediately with the message reverted <short sha> (the reverted subjects follow in the body); with a session it is recorded as an atomic commit like any other pick and takes the message given to finish. A conflict with later changes rejects the undo and modifies nothing. Several hashes are undone newest first. Merge and root commits are refused.

About

Automatically track per-file-edit commits and push them all at once when you're done. Maximum repeatability. Minimum annoyance.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages