Magit-style PORCELAIN layer for the TUI — the pure, testable half of the
C-x g status buffer. Everything runs over the native git CLI via
internal.git/run-git; nothing here touches a terminal.
Three surfaces:
status-model — one snapshot of the repo: head/upstream facts plus the
magit sections (untracked / unstaged / staged /
unmerged / stashes / unpushed / unpulled / recent
commits).stage-file!, unstage-file!, discard-file!,
commit!, push!, pull!, fetch!, branch and
stash verbs. Each returns {:ok? bool :msg str} so
the dialog can echo success/failure verbatim.status-rows — PURE projection of a model (+ the expanded-diff set)
into renderable row maps; the dialog only paints and
moves a cursor over what this returns.Keeping the model/actions/rows split pure means the whole magit feature is exercisable in tests against a throw-away repo, with zero lanterna.
Magit-style PORCELAIN layer for the TUI — the pure, testable half of the
C-x g status buffer. Everything runs over the native `git` CLI via
`internal.git/run-git`; nothing here touches a terminal.
Three surfaces:
- `status-model` — one snapshot of the repo: head/upstream facts plus the
magit sections (untracked / unstaged / staged /
unmerged / stashes / unpushed / unpulled / recent
commits).
- actions — `stage-file!`, `unstage-file!`, `discard-file!`,
`commit!`, `push!`, `pull!`, `fetch!`, branch and
stash verbs. Each returns `{:ok? bool :msg str}` so
the dialog can echo success/failure verbatim.
- `status-rows` — PURE projection of a model (+ the expanded-diff set)
into renderable row maps; the dialog only paints and
moves a cursor over what this returns.
Keeping the model/actions/rows split pure means the whole magit feature is
exercisable in tests against a throw-away repo, with zero lanterna.(commit! root message {:keys [amend?]})Refuses an empty message and — like magit's Nothing staged guard — a
non-amend commit over a clean index (a bare git commit failure would echo
a whole status dump). The index guard only applies when HEAD resolves:
diff --cached --quiet reports clean on an unborn branch even with staged
files.
Refuses an empty message and — like magit's `Nothing staged` guard — a non-amend commit over a clean index (a bare `git commit` failure would echo a whole status dump). The index guard only applies when HEAD resolves: `diff --cached --quiet` reports clean on an unborn branch even with staged files.
(commit-diff-lines root {:keys [sha]})Diff body for one commit row, as plain lines (for the TAB fold): the patch
git show produces for sha, kept from the first diff --git header down so
every file the commit touched is named.
Diff body for one commit row, as plain lines (for the TAB fold): the patch `git show` produces for `sha`, kept from the first `diff --git` header down so every file the commit touched is named.
(create-branch! root name)Create AND check out name (magit's b c).
Create AND check out `name` (magit's `b c`).
(discard-file! root {:keys [path area]})Magit's k: throw the change away.
git clean -f)git checkout -- path) — the staged half of the
same file survives, exactly like magitMagit's `k`: throw the change away.
- untracked file → delete it (`git clean -f`)
- UNSTAGED change → restore the INDEX version in the worktree only
(`git checkout -- path`) — the staged half of the
same file survives, exactly like magit
- staged change in HEAD → restore the HEAD version in index AND worktree
- added, not in HEAD → unstage only (the file survives as untracked —
never silently delete content git has never stored).(file-diff-lines root {:keys [path area]})Diff body for one file row, as plain lines (for the TAB fold).
area picks the side: :staged → diff --cached, :unstaged/:unmerged →
diff, :untracked → the file's own content rendered as additions.
Diff body for one file row, as plain lines (for the TAB fold). `area` picks the side: :staged → `diff --cached`, :unstaged/:unmerged → `diff`, :untracked → the file's own content rendered as additions.
(first-selectable rows idx)Index of the first selectable row at-or-after idx (wrapping search both
directions handled by the caller); nil when nothing is selectable.
Index of the first selectable row at-or-after `idx` (wrapping search both directions handled by the caller); nil when nothing is selectable.
(head-rows {:keys [branch detached? head head-subject upstream? upstream ahead
behind]})The Head: / Merge: preamble of the status buffer.
The `Head:` / `Merge:` preamble of the status buffer.
(head-subject root)Subject line of HEAD's commit, or nil on an unborn branch.
Subject line of HEAD's commit, or nil on an unborn branch.
(last-commit-message root)Full message of HEAD's commit (for amend pre-fill), or nil.
Full message of HEAD's commit (for amend pre-fill), or nil.
(load-repos repo-entries)Attach a fresh :model snapshot to every repo entry — one status-model
read per root. The refresh path of a multi-root buffer.
Attach a fresh `:model` snapshot to every repo entry — one `status-model` read per root. The refresh path of a multi-root buffer.
(local-branches root)Local branches as [{:name :current?}], current first.
Local branches as `[{:name :current?}]`, current first.
(multi-status-rows repos expanded diff-fn)Rows for one OR many repos. repos is [{:root :label :draft? :model}]
(:model a status-model, nil outside a git repo). ONE repo renders
exactly like status-rows — the single-root buffer looks unchanged —
while several repos each get a :repo header row. EVERY row is tagged
with its :root so the dialog's verbs route to the right repo.
expanded is a set of [root area path] triples; diff-fn receives the
root-tagged file row.
Rows for one OR many repos. `repos` is `[{:root :label :draft? :model}]`
(`:model` a `status-model`, nil outside a git repo). ONE repo renders
exactly like `status-rows` — the single-root buffer looks unchanged —
while several repos each get a `:repo` header row. EVERY row is tagged
with its `:root` so the dialog's verbs route to the right repo.
`expanded` is a set of `[root area path]` triples; `diff-fn` receives the
root-tagged file row.(next-selectable rows idx dir)Index of the nearest selectable row moving from idx by dir (+1/-1);
returns idx unchanged when there is none further that way.
Index of the nearest selectable row moving from `idx` by `dir` (+1/-1); returns `idx` unchanged when there is none further that way.
(push! root {:keys [set-upstream? force?]})opts: :set-upstream? adds -u origin <branch>, :force? uses
--force-with-lease (never bare --force).
`opts`: :set-upstream? adds `-u origin <branch>`, :force? uses `--force-with-lease` (never bare --force).
(range-commits root range-spec)Commits in range-spec (e.g. "@{upstream}..HEAD") as [{:sha :subject}],
newest first. Empty when the range is empty or unresolvable.
Commits in `range-spec` (e.g. "@{upstream}..HEAD") as `[{:sha :subject}]`,
newest first. Empty when the range is empty or unresolvable.(recent-commits root n)Latest n commits as [{:sha :subject}], newest first.
Latest `n` commits as `[{:sha :subject}]`, newest first.
(repo-row {:keys [label root draft?]})The repo header row a multi-root buffer shows above each repo's sections.
The repo header row a multi-root buffer shows above each repo's sections.
(section-of rows section-idx)The [area path]-bearing rows covered by a :section header row —
used for section-wide stage/unstage (s/u on the header). Stops at the
next :section OR :repo header and only matches rows of the SAME repo
(:root), so a multi-root buffer never bleeds one repo's files into
another's section.
The `[area path]`-bearing rows covered by a `:section` header row — used for section-wide stage/unstage (`s`/`u` on the header). Stops at the next `:section` OR `:repo` header and only matches rows of the SAME repo (`:root`), so a multi-root buffer never bleeds one repo's files into another's section.
(selectable? {:keys [kind]})Rows the dialog cursor can land on.
Rows the dialog cursor can land on.
(stash-push! root message)Stash the working tree INCLUDING untracked files (the least surprising default for a status-buffer 'stash everything' verb). Git exits 0 with 'No local changes to save' when there is nothing to stash — surface that as a failure so the dialog doesn't claim success.
Stash the working tree INCLUDING untracked files (the least surprising default for a status-buffer 'stash everything' verb). Git exits 0 with 'No local changes to save' when there is nothing to stash — surface that as a failure so the dialog doesn't claim success.
(stashes root)Stash list as [{:ref "stash@{0}" :message}], newest first.
Stash list as `[{:ref "stash@{0}" :message}]`, newest first.
(status-model root)One full magit snapshot of the repo at root, or nil outside a repo.
{:branch :detached? :head :upstream? :upstream :ahead :behind
:head-subject
:untracked [path …]
:unstaged [{:path :code} …] ;; worktree side (porcelain Y)
:staged [{:path :code} …] ;; index side (porcelain X)
:unmerged [{:path :code} …]
:stashes [{:ref :message} …]
:unpushed [{:sha :subject} …] ;; @{upstream}..HEAD — magit's Unmerged into
:unpulled [{:sha :subject} …] ;; HEAD..@{upstream} — magit's Unpulled from
:commits [{:sha :subject} …]}
One full magit snapshot of the repo at `root`, or nil outside a repo.
{:branch :detached? :head :upstream? :upstream :ahead :behind
:head-subject
:untracked [path …]
:unstaged [{:path :code} …] ;; worktree side (porcelain Y)
:staged [{:path :code} …] ;; index side (porcelain X)
:unmerged [{:path :code} …]
:stashes [{:ref :message} …]
:unpushed [{:sha :subject} …] ;; @{upstream}..HEAD — magit's `Unmerged into`
:unpulled [{:sha :subject} …] ;; HEAD..@{upstream} — magit's `Unpulled from`
:commits [{:sha :subject} …]}(status-rows model expanded)(status-rows {:keys [untracked unstaged staged unmerged stashes commits upstream
unpushed unpulled]
:as model}
expanded
diff-fn)PURE projection: model (+ expanded set of [area path], + optional
diff-fn row→lines) → renderable rows. Every row is
{:kind :info|:section|:file|:diff|:stash|:commit|:blank :text …}.
Section order mirrors magit: unpushed commits render as
Unmerged into <upstream> and Recent commits appears ONLY when nothing
is unpushed (magit's unpushed-to-upstream-or-recent); Unpulled from <upstream> closes the buffer when the branch is behind.
PURE projection: model (+ `expanded` set of `[area path]`, + optional
`diff-fn` row→lines) → renderable rows. Every row is
`{:kind :info|:section|:file|:diff|:stash|:commit|:blank :text …}`.
Section order mirrors magit: unpushed commits render as
`Unmerged into <upstream>` and `Recent commits` appears ONLY when nothing
is unpushed (magit's `unpushed-to-upstream-or-recent`); `Unpulled from
<upstream>` closes the buffer when the branch is behind.(unstage-file! root path)Magit's u: git reset -q -- path. Unlike restore --staged this also
works on an unborn HEAD (before the first commit), exactly like magit.
Magit's `u`: `git reset -q -- path`. Unlike `restore --staged` this also works on an unborn HEAD (before the first commit), exactly like magit.
(upstream-name root)Short name of the current branch's upstream (origin/main), or nil.
Short name of the current branch's upstream (`origin/main`), or nil.
(workspace-roots ws fallback-root)Ordered repo entries the magit buffer shows for one SESSION: the primary
workspace root first, then every extra filesystem root (/fs, the dir
picker). Accepts the gateway session-workspace-info map in EITHER key
shape (in-process kebab or canonical snake wire); ws nil → one entry for
fallback-root.
DRAFT semantics: each entry's :root is the path the session actually
EDITS — for a draft that's the CLONE (primary :root, extras' :clone),
never the trunk — with :trunk carrying the real dir it was forked from
and :draft? set. Deduped by :root, order preserved.
Entry: {:root abs-path :trunk abs-path :label basename :draft? bool}.
Ordered repo entries the magit buffer shows for one SESSION: the primary
workspace root first, then every extra filesystem root (`/fs`, the dir
picker). Accepts the gateway `session-workspace-info` map in EITHER key
shape (in-process kebab or canonical snake wire); `ws` nil → one entry for
`fallback-root`.
DRAFT semantics: each entry's `:root` is the path the session actually
EDITS — for a draft that's the CLONE (primary `:root`, extras' `:clone`),
never the trunk — with `:trunk` carrying the real dir it was forked from
and `:draft?` set. Deduped by `:root`, order preserved.
Entry: `{:root abs-path :trunk abs-path :label basename :draft? bool}`.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |