Skip to content

Git Panel

Each registered project has a Git overview accessible from the topbar. Multi-repo aware: the main repo, its declared submodules, and any nested standalone repos under the project root are all discovered and shown together. Pull (--ff-only) and Push are exposed as per-repo buttons; commit, stage, branch switching, merge, rebase, and force-push remain in the chat panel where Claude can reason over diff and message.

Opening the panel

Click the Git icon in the topbar (between the file-browser and chat icons). The panel slides in from the right. On desktop a split-pane layout (repo list ↔ detail card); on mobile a drill-down navigation pattern.

A small yellow dot on the topbar Git icon indicates that at least one repo needs attention (dirty tree, behind upstream, detached HEAD, etc.). The OverviewView card shows an aggregated badge per project row — silent when everything is clean.


What's discovered

Repo discovery is a depth-4 BFS from the project root, skipping node_modules, target, dist, .venv, and similar noisy directories. Three classes of repos are recognised:

Class Identification Notes
Main repo The repo at repo_root Always shown first
Submodules Listed in .gitmodules Marked with a submodule icon
Nested independent repos .git/ exists but the path is not a registered submodule Useful for monorepo subsections that have their own .git

Submodules and nested repos are visually distinguished — submodules are part of the parent commit, nested repos are not.


Per-repo status

Every repo card shows:

Field Source
Branch name git status --porcelain=v2 --branch (branch.head)
Upstream branch.upstream
Ahead / behind branch.ab; falls back to git rev-list --left-right --count if porcelain v2 omits it
Dirty (unstaged) Count of 1 .[MDR] rows
Staged Count of 1 [MDR]. rows
Untracked Count of ? rows
Stash count git stash list \| wc -l
Last commit First line of git log -1 --format=%h %s
Detached HEAD When branch.head = (detached)
No upstream When the branch has no tracking upstream

The same badges (▲N ahead, ▼M behind, ●K dirty, ▣L staged, ?P untracked, ⚑Q stash, ⚠ detached, no upstream) appear consistently across the panel, OverviewView cards, and topbar chip.


Allowed mutations

Two safe operations are exposed from the panel; everything else is sent to the chat.

Pull (--ff-only)

POST /api/projects/{slug}/git/pull
{ "repo_path": "<relative-path>" }

Runs git -C <abs> pull --ff-only. The button is disabled when:

  • The repo has behind == 0 (nothing to pull)
  • The working tree is dirty (would block fast-forward)

If the histories have diverged the API returns { ok: false, reason: "diverged", suggestion: "… handle in chat" } and the panel suggests opening the chat to discuss merge / rebase strategy.

Push

POST /api/projects/{slug}/git/push
{ "repo_path": "<relative-path>" }

Runs git -C <abs> push. Never --force, never --force-with-lease. A non-fast-forward rejection is classified as reason: "non_fast_forward" with explicit text in the panel telling you to pull first.

What's NOT in the panel

Operation Where it lives
Commit Chat panel — Claude writes the message based on the diff
Stage / unstage individual hunks Chat panel
Switch branch Chat panel
Merge / rebase Chat panel — discuss conflicts with Claude
Force-push Chat panel — must be explicitly requested

The "Discuss in chat" button on each repo card pre-fills a chat prompt with the diff question and repo context, then opens the chat in a new session.


Real-time refresh

The file-watcher fingerprints .git/HEAD, .git/index, and .git/refs/heads/ per repo (mtime-based). When any of those change — whether from an internal action, an external git fetch, or a commit you made in another terminal — only that specific repo is rescanned (not the whole project), and a GitStateUpdated { repo_path } event is broadcast over the project's WebSocket.

A 30-second safety-net poll catches missed events.


Path safety

Caller-supplied repo paths are resolved against the canonicalised project root with traversal protection:

  • .. segments rejected
  • Absolute paths outside the project tree rejected
  • Symlinks that resolve outside the tree rejected

All git invocations use Command::new("git") with arg-arrays — never sh -c — so there's no shell-injection surface even if the path were attacker-controlled.


API reference

Method Path Description
GET /api/projects/{slug}/git Full GitState (cached after first scan)
POST /api/projects/{slug}/git/pull Run pull --ff-only
POST /api/projects/{slug}/git/push Run push

All require bearer-token auth. Body for the mutations is { "repo_path": "<rel>" }.