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`).
(current-branch root)Short name of the current branch (main), or nil on a detached/unborn HEAD.
Short name of the current branch (`main`), or nil on a detached/unborn HEAD.
(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.
(gerrit-push! root {:keys [remote branch topic dry-run? no-verify?]})Push HEAD to Gerrit for review (refs/for/<branch>) — a regular push, just
carrying an optional Gerrit topic. opts: :remote :branch :topic :dry-run?
:no-verify?. remote/branch default to the detected Gerrit remote and
upstream branch.
Push HEAD to Gerrit for review (`refs/for/<branch>`) — a regular push, just carrying an optional Gerrit topic. `opts`: :remote :branch :topic :dry-run? :no-verify?. `remote`/`branch` default to the detected Gerrit remote and upstream branch.
(gerrit-remote root)Name of the remote that talks to a Gerrit server, or nil. Prefers an explicit
gerrit remote, then any remote whose URL smells like Gerrit, then falls back
to the first remote when a .gitreview file is present.
Name of the remote that talks to a Gerrit server, or nil. Prefers an explicit `gerrit` remote, then any remote whose URL smells like Gerrit, then falls back to the first remote when a `.gitreview` file is present.
(gerrit-target-branch root)Branch a Gerrit review targets: the upstream branch (minus its remote), else
the current branch, else master.
Branch a Gerrit review targets: the upstream branch (minus its remote), else the current branch, else `master`.
(gerrit? root)True when this repo appears to target a Gerrit server.
True when this repo appears to target a Gerrit server.
(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.
(log-graph-lines root)(log-graph-lines root {:keys [all? max] :or {max 250}})Pretty git log --graph for the fullscreen log viewer, as ANSI-colored
lines (git's own --color=always graph). opts:
:all? include every ref, not just HEAD's history (default false):max commit cap (default 250).
Returns a vec of lines; a one-line notice when the repo has no commits.Pretty `git log --graph` for the fullscreen log viewer, as ANSI-colored lines (git's own `--color=always` graph). `opts`: - `:all?` include every ref, not just HEAD's history (default false) - `:max` commit cap (default 250). Returns a vec of lines; a one-line notice when the repo has no commits.
(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-section rows idx dir)Index of the nearest :section/:repo header from idx moving by dir
(+1/-1); idx unchanged when there is none that way. Magit's n/p
section-to-section jump.
Index of the nearest `:section`/`:repo` header from `idx` moving by `dir` (+1/-1); `idx` unchanged when there is none that way. Magit's `n`/`p` section-to-section jump.
(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? remote dry-run? no-verify?]})opts: :remote (default "origin"), :set-upstream? adds -u <remote> <branch>,
:force? uses --force-with-lease (never bare --force), :dry-run? adds
--dry-run, :no-verify? adds --no-verify to skip pre-push hooks.
`opts`: :remote (default "origin"), :set-upstream? adds `-u <remote> <branch>`, :force? uses `--force-with-lease` (never bare --force), :dry-run? adds `--dry-run`, :no-verify? adds `--no-verify` to skip pre-push hooks.
(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.
(refs-for-spec branch topic)PURE: the Gerrit push refspec HEAD:refs/for/<branch>, with an optional
%topic=<topic> appended when topic is non-blank.
PURE: the Gerrit push refspec `HEAD:refs/for/<branch>`, with an optional `%topic=<topic>` appended when `topic` is non-blank.
(remotes root)Configured remotes as [{:name :url}] (the PUSH url), in git's own order.
Deduplicated so a remote with distinct fetch/push urls shows once.
Configured remotes as `[{:name :url}]` (the PUSH url), in git's own order.
Deduplicated so a remote with distinct fetch/push urls shows once.(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.
(section-open? expanded area)Is area's section currently expanded? expanded carries [:section area]
toggle keys that FLIP a section from its default — a default-collapsed
section (stash list, commit log) opens when its key is present; a
default-open working-tree section closes when its key is present.
Is `area`'s section currently expanded? `expanded` carries `[:section area]` toggle keys that FLIP a section from its default — a default-collapsed section (stash list, commit log) opens when its key is present; a default-open working-tree section closes when its key is present.
(selectable? {:keys [kind stageable?]})Rows the dialog cursor can land on. A :diff row is landable only when it is
:stageable? (a hunk of a staged/unstaged file), so plain diff peeks stay
scroll-only.
Rows the dialog cursor can land on. A `:diff` row is landable only when it is `:stageable?` (a hunk of a staged/unstaged file), so plain diff peeks stay scroll-only.
(split-diff-hunks lines)PURE: split a unified diff (as lines, WITH its diff --git/index/---/
+++ preamble) into {:header [lines] :hunks [[lines] ...]} — the preamble
shared by every hunk, then one line-vector per @@ hunk. A valid one-hunk
patch for hunk N is therefore (into header (nth hunks N)), which git apply
accepts. Header is everything before the first @@; each hunk runs from its
@@ line up to (not including) the next one.
PURE: split a unified diff (as `lines`, WITH its `diff --git`/`index`/`--- `/
`+++ ` preamble) into `{:header [lines] :hunks [[lines] ...]}` — the preamble
shared by every hunk, then one line-vector per `@@` hunk. A valid one-hunk
patch for hunk N is therefore `(into header (nth hunks N))`, which `git apply`
accepts. Header is everything before the first `@@`; each hunk runs from its
`@@` line up to (not including) the next one.(stage-hunk! root {:keys [path hunk]})Magit's s on a diff HUNK: stage just hunk hunk (0-based, in @@ order) of
path's UNSTAGED diff — reconstruct that one hunk into a patch and
git apply --cached it, leaving the file's other hunks unstaged.
Magit's `s` on a diff HUNK: stage just hunk `hunk` (0-based, in `@@` order) of `path`'s UNSTAGED diff — reconstruct that one hunk into a patch and `git apply --cached` it, leaving the file's other hunks unstaged.
(stash-diff-lines root {:keys [ref]})Diff body for one stash row, as plain lines (for the TAB fold): the patch
git stash show -p produces for ref, from the first diff --git header
down so every file the stash touched is named.
Diff body for one stash row, as plain lines (for the TAB fold): the patch `git stash show -p` produces for `ref`, from the first `diff --git` header down so every file the stash touched is named.
(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.
(unstage-hunk! root {:keys [path hunk]})Magit's u on a staged diff HUNK: unstage just hunk hunk (0-based) of
path's STAGED diff — reverse-apply that one hunk to the index.
Magit's `u` on a staged diff HUNK: unstage just hunk `hunk` (0-based) of `path`'s STAGED diff — reverse-apply that one hunk to the index.
(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.
(visit-file-lines root path)Working-tree content of path (relative to root) as plain lines for the
fullscreen RET-visit viewer, or nil when it isn't a readable regular file
(a deleted/renamed entry has no working-tree body — the caller falls back to
the diff).
Working-tree content of `path` (relative to `root`) as plain lines for the fullscreen `RET`-visit viewer, or nil when it isn't a readable regular file (a deleted/renamed entry has no working-tree body — the caller falls back to the diff).
(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 |