Skip to content

feat(git): git tools on the terminal toolbar — fetch, fast-forward pull, status, switch - #177

Merged
mortenaslo merged 2 commits into
mainfrom
feat/git-tools
Nov 6, 2026
Merged

mortenaslo merged 2 commits into
mainfrom
feat/git-tools

Conversation

@mortenaslo

Copy link
Copy Markdown
Contributor

Summary

When a session's folder is a git repo, its terminal toolbar shows a chip — ⎇ main ● ↑2 ↓5 (branch, dirty dot, ahead/behind) — just left of ▶. Clicking it opens a menu: a summary header (main → origin/main · ↑2 ↓5 · 3 changed), Fetch, Pull (fast-forward only), Status…, Switch branch ▸, Copy branch name and, when code.cmd is on PATH, Open in VS Code. Hidden for SSH sessions.

Why

Checking whether a repo is behind, pulling, or switching branch meant typing into the terminal — and in a Claude session that text lands in Claude's prompt.

Implementation notes

  • Deliberately read-mostly: no commit, push or merge. A Claude session is usually editing the same tree; a one-click history write from outside its conversation is a race and an unrecoverable misclick. Pull is --ff-only --no-rebase — on a diverged branch or a conflicting local edit git refuses, and a panel under the toolbar shows git's own output plus a short hint (GitStatusText.FailureHint).
  • Nothing is typed into the terminal — every operation is its own background git process via GitService.
  • One status call now feeds everything. The poll was branch --show-current + status --porcelain (two spawns); it is now one status --porcelain=v2 --branch, which also yields upstream, ahead/behind and the change list. Since ~90% of a git call is process start-up, that is cheaper and richer. GetGitInfoAsync keeps its old contract on top. New SessionViewModel fields follow GitBranch: plain properties, announced only through GitInfoVersion.
  • Every git spawn sets GIT_TERMINAL_PROMPT=0 (an HTTPS credential prompt could never be answered and would sit until the timeout); read-only calls also set GIT_OPTIONAL_LOCKS=0, so the background poll no longer takes index.lock and breaks a git command the user or Claude runs at the same moment. Both are merged into WSLENV so the git inside WSL sees them.
  • A timed-out git run now kills the whole process tree (git-remote-https / ssh children used to outlive it).
  • Switch runs switch --no-guess -- <name>: -- because a ref can legally be named -x; --no-guess so a remote-only name doesn't silently create a tracking branch.
  • One operation per repo at a time (keyed by RepoRoot, so worktrees share it); afterwards every pane on that repo re-reads git state, since a fetch moves only remote refs, which GitRepoWatcher never sees.
  • Also: both launch-failure paths now drop the pane's _runControls / _gitPanes entries (the run-control leak predates this branch).
  • CLAUDE.md: new "Git tools on the terminal toolbar" section and a GitStatusText row.

Test plan

  • GitToolsTests: porcelain v2 parser on captured output (incl. CRLF, detached, unmerged, upstream gone); wording; argv round-tripped through CommandLineToArgvW (hostile and option-shaped branch names); end to end against a throwaway bare remote — fast-forward, refusing a diverged branch even with pull.rebase=true, refusing to overwrite a local edit, the -x branch, the remote-only branch --no-guess must not invent.
  • Mutation: removing -- or --no-guess fails 6 tests each. (Removing --no-rebase fails none — git 2.54 already lets --ff-only win; kept as a guard for other versions.)
  • Full suite after rebasing on current main: 714/714.
  • Manual: chip appears on a repo session, hidden on SSH and non-repo folders; Fetch toast; Pull on a branch that is behind; Pull refused with a conflicting local edit (panel + hint); Status…; Switch branch incl. a name with _; long branch name in a narrow 2×2 pane.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

mortenaslo and others added 2 commits November 6, 2026 10:17
…ll, status, switch

When a session's folder is a git repo, its terminal toolbar shows a chip,
"⎇ main ● ↑2 ↓5" (branch, dirty dot, ahead/behind). Clicking it opens a menu:
a summary header, Fetch, Pull (fast-forward only), Status…, Switch branch ▸,
Copy branch name and, when code.cmd is on PATH, Open in VS Code. Hidden for
SSH sessions.

Deliberately read-mostly: no commit, push or merge. A Claude session is
usually editing the same tree, and a one-click history write from outside its
conversation is a race and an unrecoverable misclick. Pull is
--ff-only --no-rebase; on a diverged branch or a conflicting local edit git
refuses and a panel under the toolbar shows git's output plus a short hint.
Nothing is typed into the terminal: every operation is its own background git
process.

- The poll is now one `status --porcelain=v2 --branch` instead of
  branch --show-current + status --porcelain (two spawns), and also yields
  upstream, ahead/behind and the change list. GetGitInfoAsync keeps its old
  contract on top. New SessionViewModel fields follow GitBranch: plain
  properties announced only through GitInfoVersion.
- Every git spawn sets GIT_TERMINAL_PROMPT=0 (a credential prompt can never
  be answered); read-only calls also set GIT_OPTIONAL_LOCKS=0 so a background
  poll no longer takes index.lock and breaks a command the user runs at the
  same moment. Both are merged into WSLENV so the git inside WSL sees them.
- A timed-out git run kills the whole process tree (git-remote-https / ssh
  children used to survive).
- Switch runs `switch --no-guess -- <name>` (verified with a branch named -x).
- One operation per repo at a time, keyed by RepoRoot; afterwards every pane
  on that repo re-reads git state, since a fetch only moves remote refs.

Tests: GitToolsTests — porcelain v2 parser on captured output, wording, argv
round-tripped through CommandLineToArgvW, and end to end against a throwaway
bare remote (fast-forward; refusing a diverged branch with pull.rebase=true;
refusing to overwrite a local edit; the -x branch; a remote-only branch
--no-guess must not invent). Removing -- or --no-guess fails 6 tests each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
Both launch-failure paths (WebView2 start, PTY start) removed the pane from the
grid but left the entries BuildTerminalWrapper had registered in _runControls
and _gitPanes. The run-control leak predates this branch; the git one would
have joined it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
@mortenaslo
mortenaslo merged commit 7cf6894 into main Nov 6, 2026
1 check passed
@mortenaslo
mortenaslo deleted the feat/git-tools branch November 6, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant