-
Notifications
You must be signed in to change notification settings - Fork 0
Git Service Core
src/core/git-service.ts is the single programmatic gateway to git for the whole app: status, staging, commits, branches, sync (push/pull/publish), diff payloads, and worktrees. It is deliberately electron-free — it imports only node:child_process, node:fs, node:path, and shared types — so the Electron main process and the Server Edition boot the exact same implementation. Every call shells out to the system git binary through an argv array, never a shell string, and it is remote-agnostic: GitHub, Gitea, or any origin behave alike.
| Concern | Entry points |
|---|---|
| Repository & path resolution |
findRepoRoot, resolveRepoPath
|
| Diff payloads (for the diff node) |
diffInfo, showFromRef, readWorkingTree
|
| Working-tree/index state |
gitStatus, parseGitStatus, stageChanges, stagedDiff
|
| Commits & history |
commitChanges, recentCommits
|
| Branches |
currentBranch, listBranches, createBranch, checkoutBranch, deleteBranch, isValidGitRefName
|
| Sync / remote |
syncState, parseSyncState, push, pull, publish, remoteUrl, ghAuthed
|
| Worktrees |
listWorktrees, parseWorktreePorcelain, addWorktree, removeWorktree
|
-
src/core/git-service.ts— the entire core described on this page. -
src/shared/types.ts—GitResult,GitFileChange,GitFileStatus,GitBranchInfo,GitCommitInfo,GitSyncState,GitWorktreeare authored there and re-exported by the core (L140-L148), so the renderer and the main process share one vocabulary. -
src/shared/ipc.ts— the channel inventory;diffInfo: 'diff:info'is the diff entry point declared in the "Node services" block (L61). Git operations are never invoked by hardcoded channel strings anywhere else. -
src/renderer/src/components/SourceControlPanel.tsx— the primary consumer; it touches git only throughwindow.termsprawl.git.*(L3,L47-L54,L73-L123).
flowchart LR
subgraph Renderer
P["SourceControlPanel<br/>snap / worktrees / busy / error"]
end
subgraph Main["main or server process"]
H["git IPC handler<br/>composes GitPanelSnapshot"]
C["core/git-service.ts"]
end
P -->|"window.termsprawl.git.*(GitTarget)"| H
H -->|"cwd — local project"| C
H -->|"remote — ssh project"| C
C -->|"execFile('git', argv, { cwd, maxBuffer })"| G["system git binary"]
G -->|"stdout / stderr / exit code"| C
C -->|"GitResult"| H
H -->|"JSON over IPC"| P
Key nodes:
-
GitTargetis the routing key. The panel buildsremote ? { remote } : { cwd }(SourceControlPanel.tsx#L43-L45), so a local folder project sends a working directory and a remote project sends aProjectRemote; the main process "resolves the repo root on the correct side". For a remote project the same argv contract runs over ssh against the remote tree root (L16-L17). -
The handler layer is thin by design: it composes primitives from the core (
gitStatus+currentBranch+syncState+remoteUrl+ghAuthed) into aGitPanelSnapshot, and fulfils the panel'ssnapshot/worktreesreads (SourceControlPanel.tsx#L47-L54). - The core owns no state. Every function takes a repo root, runs one or two git invocations, and returns a plain value. That is why the Server Edition can reuse it unchanged.
runGit is the single spawn point (L126-L138): it always prepends -c color.ui=false, runs with the caller's cwd, and collects a 32 MB maxBuffer. It resolves rather than rejects — every failure becomes { code, stdout, stderr }, so callers never need try/catch around git.
Every function below ends in one runGit invocation; the argv column is the exact argument vector after the implicit -c color.ui=false.
| Function | argv | Failure behavior |
|---|---|---|
gitStatus |
status --porcelain |
[] on non-zero |
stagedDiff |
diff --cached |
'' on non-zero |
currentBranch |
branch --show-current |
'' on non-zero |
listBranches |
branch --format=%(HEAD)%09%(refname:short) |
[] on non-zero |
stageChanges |
add -A -- <paths...> |
GitResult passthrough |
createBranch |
checkout -b <name> |
ref-name guard, then git |
checkoutBranch |
checkout <name> |
ref-name guard, then git |
deleteBranch |
branch -D <name> |
ref-name guard; force delete |
commitChanges |
commit -m <message> |
git's own error in stderr
|
recentCommits |
log -N --pretty=%h\t%an\t%ad\t%s --date=short |
[]; limit defaults to 20 |
syncState |
status -sb --porcelain=1 |
neutral { upstream: null, ahead: 0, behind: 0 }
|
push / pull
|
push / pull
|
GitResult passthrough |
publish |
push -u origin <currentBranch> |
origin is hardcoded |
remoteUrl |
remote get-url origin |
null on non-zero |
listWorktrees |
worktree list --porcelain |
[] on non-zero |
addWorktree |
worktree add [-b <branch>] <path> |
branch guarded; path must not exist |
removeWorktree |
worktree remove [--force] <path> |
force is opt-in |
ghAuthed() is the one exception: it uses spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }) and returns status === 0 (L316-L319). It runs no git and blocks, but nothing else in this module depends on it. The panel uses it only to render the "pushing to GitHub needs you logged in" hint when the remote contains github.com (SourceControlPanel.tsx#L125, L160-L162).
The core is stateless; the meaningful state lives in shared shapes and in the panel's local guards.
-
GitResult { code, stdout, stderr }— the universal op result.runGitnormalizes spawn failures tocode ?? 1, which is what lets the panel'srun()helper branch onres.code !== 0and surfacestderras the error line (SourceControlPanel.tsx#L60-L71). -
GitFileChange { path, status, staged }— one row per porcelain entry; drives the stage/unstage toggle. -
GitSyncState { upstream, ahead, behind }— drives the↑N/↓Mbadge next to the branch name (SourceControlPanel.tsx#L174-L179). -
GitBranchInfo { name, current },GitCommitInfo { hash, author, date, subject }— list payloads. -
GitWorktree { path, branch, head }—branchstaysnullfor a detached worktree. -
GitPanelSnapshot— the composed view. Fields consumed in the read range arebranch,sync,remote, andghAuthed;branch === ''is the panel's "not a git repository" signal (SourceControlPanel.tsx#L167-L168). - Panel-local guards:
busydisables concurrent ops,error/statuscarry the last outcome, and destructive paths are gated byconfirmDiscardandconfirmRemoveWtbefore the op is issued (L36,L40,L120-L123).
diffInfo(path, base) is a separate entry point from the source-control ops — it feeds the diff node through the diff:info channel — and it is documented as never throwing: errors travel inside the payload so the renderer can render a status line (L87-L116).
sequenceDiagram
participant D as Diff node (renderer)
participant M as main: diff:info
participant S as core: diffInfo()
D->>M: { path, base: 'staged' | 'HEAD' }
M->>S: diffInfo(path, base)
S->>S: findRepoRoot(dirname(path))
alt no enclosing .git
S-->>M: { original: null, modified: null, error: NO_REPO }
else repo root found
S->>S: showFromRef(root, ':' or 'HEAD', repoPath)
S->>S: readWorkingTree(root, repoPath)
alt original and modified both null
S-->>M: { original: null, modified: null, error: IO }
else
S-->>M: { original, modified }
end
end
M-->>D: DiffInfo
Key steps:
-
findRepoRootwalks upward fromdirname(path)and returns the first directory whose.gitentry exists, ornullat the filesystem root (L34-L42). The check isexistsSync, so a.gitfile — the form used by worktrees and submodules — also terminates the walk. -
basemaps to a ref:'staged'→':'(the index), otherwise'HEAD'.showFromRefbuilds<ref>:<path>itself, with:treated as a prefix rather than a ref name (L50-L67). -
resolveRepoPathconverts an absolute path to a repo-relative one; a relative path is assumed already repo-relative (L83-L85). -
DiffInfocarriesoriginal(from the ref) andmodified(working tree), eachnullwhen unavailable.DiffErrorCodedeclaresNO_REPO | MISSING | IO, but onlyNO_REPOandIOare constructed in this module —MISSINGis a reserved code for callers. - A path absent from both the ref and the working tree is the only case that yields an error when a repo was found; a path present in only one side is a valid diff (added or deleted).
-
No shell strings, ever. Every git invocation is
execFile('git', [...])with a fixed argv array; user input only ever lands inside a single argv element. The file header states this explicitly as a design rule for the core. -
isValidGitRefName(name)is an inline guard — no extra process — applied bycreateBranch,checkoutBranch,deleteBranch, andaddWorktreewhen a branch is supplied (L223-L232,L359-L361). Rejections return{ code: 128, stderr: 'invalid branch name: …' }without spawning.- Rejects: empty, leading
-(option injection),..,@{,//, trailing/or., the literal@, and any control character, space, or one of~^:?*[\.
- Rejects: empty, leading
-
Destructive operations are opt-in.
deleteBranchusesbranch -D;removeWorktreeonly passes--forcewhen the caller asks, and the panel only does so after its inline confirm. Both the code comment and the panel reflect the same contract: force only after the caller has confirmed the discard.
These are the places where the module's "resolve, never throw" style trades precision for resilience:
-
Silent empty results.
gitStatus,listBranches,listWorktrees, andrecentCommitsall return[]on a non-zero exit. A missing repo, a broken index, and a genuinely clean tree can be indistinguishable at the call site; onlyGitPanelSnapshot.branch === ''lets the panel distinguish "not a repo". -
runGit's exit-code cast.err.code ?? 1is cast to a number, but a spawn failure (for examplegitnot onPATH) yields the string'ENOENT'. Thecode !== 0check still fails correctly, but the reported value is not a real exit status. -
Buffer ceilings.
runGitallows 32 MB of output,showFromRefallows 16 MB. A larger payload overflows into the error path — and inshowFromRefthat error is coalesced intonull, i.e. an oversized file in the index becomes anoriginal: nulldiff rather than an explicit error. -
showFromRefflattens all git errors tonull(L58-L61). Missing-from-ref, a bad ref, and a buffer overflow are the same answer. -
Porcelain v1 is parsed, not normalized.
parseGitStatustakesline[0]/line[1]as index/working-tree state andline.slice(3)as the path (L152-L172). Consequently: paths are passed through verbatim (no unquoting of git's quoted non-ASCII/escaped forms), and a rename entry (R old -> new) arrives as a single path containing the arrow.gitStatusalso does not setcore.quotepath=false;color.ui=falseis the only config override. -
One line per file, not per side. A file that is both staged and further modified in the working tree (
MM) produces exactly one entry withstaged: true; the working-tree half of that state is not represented in the change list, so the panel's stage/unstage toggle operates on the whole row. Untracked (??) entries short-circuit to{ status: 'untracked', staged: false }. -
parseSyncStateis a partial regex.^##\s+([^.\s]+)(?:\.\.\.(\S+))?(?:\s+\[ahead\s+(\d+)(?:,\s+behind\s+(\d+))?\])?(L279) is not end-anchored and the branch group excludes., so:- a dotted branch name such as
release/1.2truncates the match and yields{ upstream: null, ahead: 0, behind: 0 }; - a
[behind M]-only line (behind without ahead) does not satisfy the optional group, which starts with the literalahead, so behind is reported as0. Callers therefore cannot treatbehind === 0as proof of being up to date.
- a dotted branch name such as
-
publishassumesorigin, and takescurrentBranchfirst; if that call fails the branch string is empty and git rejects the resultingpush -u origin. -
addWorktreedoes not validate or createpath. It forwards the string as given and relies on git's own refusal when the path already exists, so callers own path resolution. -
Remote routing is a caller concern. The core always takes a plain
repoRoot; whether that root is local or on an ssh host is decided above it byGitTarget.
-
Add a new git operation: write a
runGit(repoRoot, [...])wrapper next to the existing ones, export the result type fromsrc/shared/types.tsif it is new, then add a channel insrc/shared/ipc.tsand awindow.termsprawl.git.*method. The panel calls nothing else. -
Add a new write path:
stageChangesis the only index-mutating helper in this module. The panel'sunstageanddiscardcalls (SourceControlPanel.tsx#L75,L81) have no counterpart here, so index resets and working-tree restores are fulfilled by the main-process handler — that is the layer to extend when adding index/working-tree writes. -
Change transports: because the core is electron-free and argv-based, an ssh-backed implementation of the same function signatures can be swapped in behind the
{ remote }target without touching this file's contract. -
Parser hardening:
parseGitStatus,parseSyncState, andparseWorktreePorcelainare exported pure functions, so format changes (porcelain v2 /-z, a corrected sync regex) can be made and tested in isolation from process spawning. -
AI commit messages do not belong here: the panel obtains one via
window.termsprawl.git.commitMessage(target)and drops it into the message box for review (SourceControlPanel.tsx#L92-L107), keeping generation out of the core git surface.
Sources: src/core/git-service.ts, src/core/git-service.ts, src/renderer/src/components/SourceControlPanel.tsx, src/shared/ipc.ts
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance