Introduction
git-gud (gg) is a stacked-diffs CLI for GitHub and GitLab.
It helps you split large changes into a sequence of small commits that reviewers can understand quickly. In git-gud, each commit in your stack maps to its own PR/MR, and dependencies are wired automatically.
Why this workflow exists
Stacked diffs solve a common problem: features are often too large for a single review, but splitting work manually into many dependent branches is painful.
With git-gud, you can:
- Keep reviews small and focused
- Keep moving while earlier changes are in review
- Preserve clean, logical commit history
- Land big projects incrementally without long-lived feature branches
Learn more about stacked diffs
-
Introducing git-gud — the story behind this tool, by its author
Provider support
git-gud supports:
- GitHub through
gh - GitLab through
glab
Provider selection is auto-detected from your remote URL (github.com / gitlab.com). For self-hosted instances, run gg setup and select the provider explicitly.
Getting Started
Installation
Homebrew (macOS/Linux)
brew install mrmans0n/tap/gg-stack
crates.io
cargo install gg-stack
From source
cargo install --path .
Prerequisites
Before using git-gud, make sure you have:
- Git 2.x+
- GitHub CLI (
gh) for GitHub repositories - GitLab CLI (
glab) for GitLab repositories
Authentication
Authenticate with your provider CLI first:
# GitHub
gh auth login
# GitLab
glab auth login
If authentication is missing, gg sync and gg land cannot create or merge PRs/MRs.
Note: If the auth check fails due to a network error (e.g., DNS resolution failure, connection timeout), gg will print a warning and continue. The operation may still fail later if authentication is actually required.
Initial setup
After installing, run the setup wizard in any git repository to configure git-gud:
gg setup
This interactively sets your base branch, username for branch naming, provider (auto-detected for github.com/gitlab.com), and optional lint commands. Configuration is stored per-repo in .git/gg/config.json.
Tip:
gg setupis optional — git-gud auto-detects sensible defaults. But it’s useful for setting lint commands, customizing your username prefix, or configuring self-hosted GitHub Enterprise / GitLab instances.
See Configuration for all available options.
Quick start: first stack in 2 minutes
# 1) Create a stack
gg co my-feature
# 2) Commit in small slices
git add . && git commit -m "Add data model"
git add . && git commit -m "Add API endpoint"
git add . && git commit -m "Add UI"
# 3) Inspect current stack
gg ls
# 4) Push branches and create PRs/MRs
gg sync --draft
# 5) Navigate to edit an earlier commit
gg mv 1
# ...make changes...
gg sc
# 6) Re-sync after changes
gg sync
# 7) Land approved changes
gg land --all
# 8) Clean merged stack
gg clean
For a full walkthrough with expected outputs and decision points, see Your First Stack.
Core Concepts
Stacked diffs in one sentence
A stack is a series of commits where each commit is reviewed as its own PR/MR, and each PR/MR depends on the previous one.
The git-gud model: one commit = one PR/MR
In git-gud, each commit is an “entry” in the stack:
- Entry 1 targets your base branch (for example,
main) - Entry 2 targets entry 1’s branch
- Entry 3 targets entry 2’s branch
- …and so on
That gives reviewers small units, while preserving execution order.
GG metadata trailers
Each stack commit carries stable trailers, for example:
GG-ID: c-abc1234
GG-Parent: c-1234567
GG-IDidentifies the commit itself.GG-Parentpoints to the previous stack entry’sGG-ID.- The first stack entry has no
GG-Parent.
Why this matters:
- Commit-to-PR/MR mappings stay stable across rebases
- Stack topology is recoverable from commit-local metadata
gg syncandgg reconcilecan auto-heal metadata drift after history edits
Branch naming convention
git-gud uses predictable branch names:
- Stack branch:
<username>/<stack-name> - Entry branch:
<username>/<stack-name>--<gg-id>
Example:
nacho/user-authnacho/user-auth--c-abc1234
This convention is what makes remote discovery (gg ls --remote) and reconciliation possible.
PR/MR dependency chains
Dependency chaining is automatic during gg sync:
- First PR/MR targets
main(or your configured base) - Next PR/MR targets previous entry branch
- This continues until stack head
Result: reviewers can review from bottom to top, and gg land can merge safely in order.
Immutable commits
Some commits should not be casually rewritten. History-rewriting commands —
gg squash, gg drop, gg reorder, gg split, gg unstack, gg absorb, and gg rebase —
refuse to touch the following by default:
- Merged PR/MR commits. If an entry’s PR/MR state is
Merged, rewriting it locally produces a duplicate of something already upstream. This is the only rule that catches squash-merged PRs, because their merge commit onorigin/<base>has a brand-new SHA that doesn’t share ancestry with your local commit. - Base-ancestor commits. Any commit already reachable from
origin/<base>via plain merge or rebase falls in the same bucket. When noorigin/<base>ref exists, gg falls back to the local base branch.
Exception: gg rebase silently skips both merged-PR commits and
base-ancestor commits instead of blocking, because git rebase naturally drops
them via patch-id matching. An info line (→ Skipping N merged commit(s) already on <base>) is printed when this happens.
Running one of those commands on an immutable target prints a clear error like:
error: cannot rewrite immutable commits (pass --force / --ignore-immutable to override):
#2 abc1234 Fix typo in parser (merged as !123)
#3 def5678 Bump dependency (already in origin/main)
If you genuinely want to rewrite history anyway, pass --force (or
--ignore-immutable if you prefer the longer, self-describing name). Every
rewrite command accepts both spellings. The guard still emits a warning so
scripts see that they are bypassing a safety check.
Keeping PR state fresh
Each rewrite command runs a best-effort PR-state refresh against the
configured provider just before the immutability check, so the merged-PR rule
fires even when nothing in the session has touched provider state yet. The
refresh is silent when offline / no auth is configured, in which case the
base-ancestor rule remains the only protection. Note that base-ancestor does
not catch squash-merges (those produce a new SHA on origin/<base>), so
working offline against a repo that uses squash-merge is the one case where
you can still rewrite a “merged” commit without --force. A working provider
closes that gap automatically.
Guides
This section is practical and task-oriented.
If you’re new to stacked diffs, start with Your First Stack. If you’re already using git-gud day-to-day, jump to the workflow you need (editing, remote collaboration, worktrees, landing, linting, reconcile).
Your First Stack
This walkthrough covers the full lifecycle: create → commit → sync → edit → land → clean.
1) Create a stack
gg co user-auth
This creates/switches to stack branch your-user/user-auth.
2) Build the feature in reviewable commits
git add . && git commit -m "Add user model"
git add . && git commit -m "Add auth endpoints"
git add . && git commit -m "Add login UI"
Keep each commit small and self-contained.
3) Inspect stack structure
gg ls
You should see ordered entries, each with a GG-ID.
4) Publish review chain
gg sync --draft
This pushes one branch per entry and creates one PR/MR per commit, chained by dependencies.
5) Address feedback in an older commit
gg mv 1
# edit files
git add .
gg sc
gg sc amends the current entry and rebases subsequent entries automatically.
6) Update remote PRs/MRs
gg sync
If your stack base is behind origin/<base>, gg sync warns and suggests running gg rebase first.
Use --no-rebase-check to skip this check once.
7) Land approved entries
gg land --all
Use --wait if you want git-gud to wait for CI/approvals:
gg land --all --wait
8) Cleanup
gg clean
Editing Commits in a Stack
The most common stacked-diff operation is “change commit N without losing N+1, N+2…”.
Navigate to target commit
gg mv 2
# or use gg first / gg next / gg prev / gg last
Make changes and fold them in
# after editing files
git add .
gg sc
Use gg sc --all to include unstaged changes too.
Reorder commits
Interactive:
gg reorder
Explicit order:
gg reorder --order "3,1,2"
Absorb scattered staged edits automatically
gg absorb
Useful flags:
--dry-run: preview only--and-rebase: absorb and rebase in one step--whole-file: match whole-file changes instead of hunks--squash: squash fixups directly
After major edits, run:
gg sync
Working with Remote Stacks
Use this when a stack exists on origin but not in your local checkout (new machine, pairing, takeover).
Discover remote-only stacks
gg ls --remote
Active stacks are shown first. Stacks whose PRs/MRs have all been merged appear in a separate “Landed” section at the bottom, so you can focus on work that still needs attention.
Check out a remote stack
gg co user-auth
If a local stack doesn’t exist, git-gud can reconstruct it from remote entry branches and mappings.
Typical collaboration loop
gg co teammate-feature
gg ls
# make changes
gg sync
Tips:
- Prefer
gg syncover manualgit pushto keep mappings healthy - If mappings drift, use Reconciling Out-of-Sync Stacks
Using Worktrees
Worktrees let you keep your main checkout clean while developing a stack in a dedicated directory.
Create stack in a managed worktree
gg co user-auth --worktree
Short flag:
gg co user-auth -w
Why use worktrees
- Keep your main checkout untouched
- Work on multiple stacks side by side
- Avoid stashing/switching overhead
Default path behavior
By default git-gud creates:
../<repo-name>.<stack-name>
You can change this with defaults.worktree_base_path in .git/gg/config.json.
Cleanup behavior
gg clean removes merged stacks and associated managed worktrees.
Landing and Cleanup
Use gg land to merge entries in order, from the bottom of the stack upward.
Land one approved entry
gg land
Land the whole stack
gg land --all
Wait for CI and approvals
gg land --all --wait
Land only part of a stack
gg land --until 2
# or by GG-ID / SHA
Merge strategy and provider-specific behavior
gg land --no-squash
GitLab auto-merge queue:
gg land --auto-merge
Auto-clean after landing
One-off:
gg land --all --clean
Or make it default with land_auto_clean in config.
Manual cleanup remains available:
gg clean
Linting Your Stack
gg lint runs your configured lint commands commit-by-commit across the stack.
Configure lint commands
In .git/gg/config.json:
{
"defaults": {
"lint": [
"cargo fmt --check",
"cargo clippy -- -D warnings"
]
}
}
Run lint manually
gg lint
Run only up to a specific entry:
gg lint --until 2
Run lint during sync
gg sync --lint
If lint fails during gg sync --lint, sync is aborted and git-gud restores repository state to the pre-sync snapshot.
Skip lint for one sync (even if enabled by default):
gg sync --no-lint
Reconciling Out-of-Sync Stacks
Use reconcile when stack metadata and remote state diverge.
Common causes:
- Someone pushed with
git pushinstead ofgg sync - A stack was edited across machines and mappings got stale
- Commits exist without GG-ID trailers
Preview changes safely
gg reconcile --dry-run
Apply reconciliation
gg reconcile
Reconcile can:
- Add missing GG-IDs to stack commits (via rebase)
- Map existing PRs/MRs to the right GG-IDs in config
After reconciling, run:
gg ls --refresh
gg sync
Agent Skills
git-gud follows the open Agent Skills standard. AI coding agents with shell access — including Codex, Claude Code, Cursor, Gemini CLI, VS Code integrations, and others — can use gg for stacked-diff workflows.
What’s included
The integration provides one unified skill:
| Skill | Description |
|---|---|
gg | Use gg with GitHub PRs (gh CLI) or GitLab MRs (glab CLI, merge trains) |
Each skill includes:
- SKILL.md — concise instructions with agent operating rules
- reference.md — command reference and JSON schemas
- examples/ — step-by-step workflow walkthroughs
Installation
1) Generic install (recommended)
npx skills add mrmans0n/git-gud
The skills CLI installs the skill into the agent setup it detects. If it cannot decide, it prompts you to choose where to install it.
2) Install for a specific agent
Use --agent when you already know the host you want to target:
npx skills add mrmans0n/git-gud --agent codex
npx skills add mrmans0n/git-gud --agent claude-code
npx skills add mrmans0n/git-gud --agent cursor
npx skills add mrmans0n/git-gud --agent gemini-cli
For a shared repository setup, run the command from the project root. For a user-level install, add --global.
3) Claude Code marketplace
Claude Code users can also install git-gud as a plugin:
claude plugin marketplace add https://github.com/mrmans0n/git-gud
claude plugin install git-gud
4) Claude Code local checkout
Use this when launching Claude Code directly from a local git-gud checkout:
claude --plugin-dir /path/to/git-gud
5) Claude Code project-level config (.claude/settings.json)
Use this when you want the local checkout enabled by default for a repository:
{
"plugins": [
{
"name": "git-gud",
"path": "/path/to/git-gud"
}
]
}
6) Manual setup for compatible tools
Tools that support Agent Skills can also load the repo’s skills/gg/ directory directly. Use this fallback if your agent does not use the skills CLI yet, or if you want to manage skill files yourself.
How agents typically use gg
A practical AI-assisted stacked-diff workflow looks like this:
- Agent creates or switches to a stack (
gg co ..., ideally with a worktree) - Agent makes small commits and keeps each commit focused
- Agent syncs the stack (
gg sync) so PRs/MRs are created/updated in order - Agent iterates on review feedback (amend/reorder/re-sync)
- Agent asks for explicit user confirmation before
gg land
This keeps work reviewable while preserving user control over merges.
JSON output for tool-driven agents
For machine-readable parsing, gg supports --json on key commands:
gg ls --jsongg sync --jsongg land --jsongg clean --jsongg lint --json
Use these outputs in agents and automation for reliable state checks and decisions. For full response schemas, see each skill’s reference.md.
Safety model (required behavior)
When using AI agents with gg, keep these rules:
- Never land without explicit user confirmation
- Never run
git add -Ablindly (stage only reviewed/intended files) - Prefer worktrees for isolation (
gg co --wt) - Use structured output (
--json) when automation must parse command results
Skill references
For full operational details, prompts, and examples:
- Unified skill:
skills/gg/SKILL.md
File structure
.claude-plugin/
plugin.json # Plugin manifest
skills/
gg/
SKILL.md # Unified GitHub + GitLab skill
reference.md # Command reference + JSON schemas
examples/
basic-flow.md # Provider-agnostic feature workflow
multi-commit.md # Absorb, reorder, lint
merge-train.md # GitLab merge train workflow
Command Reference
This section is a command-by-command reference based on gg --help and gg <command> --help.
Unlike the guides, this section is organized by command surface and flags. Each page still includes practical examples.
Command groups
- Stack lifecycle:
co,ls,sync,land,clean - Editing:
mv,first,last,prev,next,sc,absorb,reorder,split,unstack,rebase - Utilities:
lint,setup,reconcile,continue,abort,completions
gg co
Create a new stack, switch to an existing local stack, or check out a remote stack by name.
gg co [OPTIONS] [STACK_NAME]
Options
-b, --base <BASE>: Base branch to use (default auto-detected: main/master/trunk)-w, --worktree: Create or reuse a managed worktree for this stack
Examples
# Create/switch stack
gg co user-auth
# Create stack based on a specific branch
gg co user-auth --base develop
# Create stack in worktree
gg co user-auth --worktree
gg ls
List the current stack, all local stacks, or remote-only stacks.
When the stack base is behind origin/<base>, output includes a ↓N indicator (N = commits behind).
gg ls [OPTIONS]
Options
-a, --all: Show all local stacks-r, --refresh: Refresh PR/MR status from remote--remote: List remote stacks not checked out locally. Stacks whose PRs/MRs are all merged are shown in a separate “Landed” section at the bottom with a✓marker--json: Print structured JSON output (for scripts and automation). Automatically performs a best-effort refresh of PR/MR state from the provider API, sopr_stateandci_statusfields are populated without needing--refresh.
Examples
# Current stack status
gg ls
# All local stacks
gg ls --all
# Remote stacks (active first, then landed)
gg ls --remote
# Refresh status badges from provider
gg ls --refresh
# Structured JSON for automation
gg ls --json
gg ls --all --json
gg ls --remote --json
gg log
Show a smartlog-style view of the current stack.
gg log is stack-scoped: it renders just the current stack as a tree,
with each commit’s position, short SHA, title, PR/MR state, CI badge, and a
<- HEAD marker on the currently-checked-out commit. For a cross-stack
overview use gg ls --all.
gg log [OPTIONS]
Options
-r, --refresh: Refresh PR/MR status from the remote before rendering.--json: Print structured JSON output (for scripts and automation). Automatically performs a best-effort refresh of PR/MR state from the provider API, sopr_stateandci_statusfields are populated without needing--refresh.
Example output
my-feature (3 commits, base: main)
├── [1] abc1234 feat: add parser open #101 ✓
│ #101
├── [2] def5678 feat: wire CLI flag open #102 ●
│ #102
└── [3] 9abcdef test: coverage for edge cases not pushed <- HEAD
JSON shape
{
"version": 1,
"log": {
"stack": "my-feature",
"base": "main",
"current_position": 3,
"entries": [
{
"position": 1,
"sha": "abc1234",
"title": "feat: add parser",
"gg_id": "c-abc1234",
"gg_parent": null,
"pr_number": 101,
"pr_state": "open",
"approved": false,
"ci_status": "success",
"is_current": false,
"in_merge_train": false,
"merge_train_position": null
}
]
}
}
Entry fields match gg ls --json so consumers can share
parsers across both commands.
See also
gg ls— current stack details with more summary metrics, plus--alland--remotemodes.
gg inbox
gg inbox shows an actionable repository-wide triage view for all local stacks. Instead of inspecting stacks one by one, it groups PRs or MRs by what they need right now.
Use it when you want quick answers to questions like:
- which PRs are ready to land
- which ones are blocked on CI
- where changes were requested
- which stacks have fallen behind their base
Usage
gg inbox
gg inbox --all
gg inbox --json
Buckets
gg inbox classifies each PR or MR into exactly one bucket, in priority order:
ready_to_landchanges_requestedblocked_on_ciawaiting_reviewbehind_basedraftmerged(only with--all)
Classification notes
- A canceled CI run counts as
blocked_on_ci. - If remote refresh fails transiently, the entry stays visible instead of disappearing, so the inbox does not look empty because of a temporary provider error.
behind_baseis computed from the real stack tip versusorigin/<base>, not from the state of your local base branch.
Example human output
The labels and number prefixes adapt to the detected provider.
GitHub:
Inbox (3 items across 2 stacks)
Ready to land (1):
auth #2 abc1234 Add login button stack/auth PR #41
Blocked on CI (1):
auth #3 def5678 Add login API stack/auth PR #42 ⏳
Awaiting review (1):
billing #1 9876abc Add invoice export stack/billing PR #51
GitLab:
Inbox (2 items across 1 stack)
Ready to land (1):
auth #2 abc1234 Add login button stack/auth MR !41
Awaiting review (1):
auth #3 def5678 Add login API stack/auth MR !42
JSON
With --json, gg inbox returns a versioned response designed for automation and MCP.
Example:
{
"version": 1,
"total_items": 2,
"buckets": {
"ready_to_land": [
{
"stack_name": "auth",
"position": 1,
"sha": "abc1234",
"title": "Add login",
"pr_number": 42,
"pr_url": "https://github.com/org/repo/pull/42",
"ci_status": "success",
"behind_base": null
}
],
"blocked_on_ci": [
{
"stack_name": "auth",
"position": 2,
"sha": "def5678",
"title": "Add login API",
"pr_number": 43,
"pr_url": "https://github.com/org/repo/pull/43",
"ci_status": "running",
"behind_base": 2
}
]
}
}
Per-entry fields
stack_name: stack nameposition: commit position inside the stacksha: short SHAtitle: commit titlepr_number: PR or MR numberpr_url: PR or MR URLci_status:pending,running,success,failed,canceled,unknown, or omittedbehind_base: number of commits behindorigin/<base>, ornull
Flags
--all: include items already marked asmerged--json: emit structured output for tooling and MCP
Relationship to other commands
gg lsshows detailed status for the current stackgg loggives you a smartlog view of the current stackgg inboxis for cross-stack triage across multiple stacks
gg sync
Push entry branches and create/update PRs/MRs for the current stack.
gg sync [OPTIONS]
Options
-d, --draft: Create new PRs/MRs as draft (does not affect existing PRs/MRs)-f, --force: Force push even if remote is ahead--update-descriptions: Update PR/MR descriptions from commit messages--update-title: Update PR/MR titles from commit messages-l, --lint: Run lint before sync (aborts sync on lint failure and restores repository state to the pre-sync snapshot)--no-lint: Disable lint before sync (overrides config default)--no-rebase-check: Skip checking whether your stack base is behindorigin/<base>--no-verify: Skip the pre-push hook for pushes performed by this sync (forwardsgit push --no-verify). Opt-in per invocation; does not affect other hooks.-u, --until <UNTIL>: Sync up to target commit (position, GG-ID, or SHA)--json: Output structured JSON for automation (suppresses human/progress output)
Before pushing, gg sync checks whether your stack base is behind origin/<base>. If it is behind by at least the configured threshold, git-gud warns and suggests rebasing first (gg rebase).
When you run gg sync --lint, lint runs before any push/PR updates. If lint fails, sync aborts immediately and git-gud restores your repository to the pre-sync snapshot.
Before pushing, gg sync also normalizes commit metadata (GG-ID and GG-Parent) for the whole stack. This normalization is always enforced during sync (including adding missing GG-ID trailers) to keep stack identity and PR/MR mappings stable.
If an existing mapped PR/MR is attached to the wrong source branch (for
example after moving commits into a new stack with gg unstack), providers
cannot retarget that source branch in place. gg sync creates a replacement
PR/MR with the correct branch, updates the local mapping, comments on the old
PR/MR, and closes it. In JSON output that entry uses action "recreated".
You can control this behavior with config:
defaults.sync_auto_rebase(sync.auto_rebase): automatically rungg rebasebefore sync when behind threshold is reacheddefaults.sync_behind_threshold(sync.behind_threshold): minimum number of commits behind before warning/rebase logic applies (0disables the check)
Examples
# First publish as drafts
gg sync --draft
# Sync only first two entries
gg sync --until 2
# Refresh PR/MR descriptions after commit message edits
gg sync --update-descriptions
# Also update PR/MR titles to match commit subjects
gg sync --update-title
# Run lint as part of sync
gg sync --lint
# Skip behind-base check once
gg sync --no-rebase-check
# Machine-readable output
# (useful in scripts/agents)
gg sync --json
# Skip pre-push hooks for this sync only
gg sync --no-verify
Target Branch Resolution
When computing the target branch for each PR/MR, gg sync walks backwards through predecessor entries and skips any that are already merged or closed. If all predecessors have been merged, the target falls back to stack.base. This ensures downstream MRs are correctly retargeted after an upstream MR is merged — whether merged via gg land or directly in the provider UI.
PR/MR Body Ownership
When gg sync creates a new PR/MR, the generated description is wrapped in invisible HTML comment markers:
<!-- gg:managed:start -->
(generated content from commit message / template)
<!-- gg:managed:end -->
On subsequent syncs with --update-descriptions (or when sync_update_descriptions is enabled in config), only the content inside the managed block is regenerated. Any text you add above or below the markers on GitHub/GitLab is preserved across syncs.
This means you can safely:
- Add review checklists above or below the managed block
- Write reviewer notes that survive re-syncs
- Check/uncheck task boxes outside the managed section
Content inside the managed block (the generated description) is regenerated on every sync. If your PR template includes a checklist, place persistent checklists outside the markers after creation.
Legacy PRs (created before this feature) have no managed markers. gg sync will skip body updates for these PRs and log a warning, to avoid overwriting manual edits.
Stack navigation comments
If defaults.stack_nav_comments is enabled in .git/gg/config.json, every
full gg sync (no --until) reconciles a managed comment on each PR/MR in
the stack. The comment shows all entries in the stack in bottom-up order,
with a 👉 marker on the entry that PR corresponds to — letting reviewers see
where they are in the chain and click through to siblings.
The comment is identified by a hidden HTML marker (<!-- gg:stack-nav -->)
and never touches comments git-gud didn’t create. Disabling the setting and
re-syncing cleans up any previously-posted comments automatically.
Merged or closed PRs are left alone — gg sync never modifies comments on
historical PRs.
When running with --json, each entry includes an optional nav_comment_action
field (one of "created", "updated", "unchanged", "deleted", "error")
when a reconcile decision was made.
Example JSON (shape):
{
"version": 1,
"sync": {
"stack": "my-stack",
"base": "main",
"rebased_before_sync": false,
"metadata": {
"gg_ids_added": 0,
"gg_parents_updated": 1,
"gg_parents_removed": 0
},
"entries": [
{
"position": 1,
"sha": "abc1234",
"title": "Add feature",
"gg_id": "c-abc1234",
"branch": "user/my-stack--c-abc1234",
"action": "created",
"pr_number": 42,
"pr_url": "https://github.com/org/repo/pull/42",
"draft": false,
"pushed": true,
"error": null
}
]
}
}
Navigation (mv, first, last, prev, next)
These commands move HEAD within your stack without manual rebase gymnastics.
gg mv <TARGET>
Move to a specific entry by:
- Position (1-indexed)
- GG-ID (
c-...) - Commit SHA
gg mv 1
gg mv c-abc1234
gg mv a1b2c3d
Relative navigation
gg first # first entry
gg last # stack head
gg prev # previous entry
gg next # next entry
gg sc
Squash local changes into the current stack commit.
gg sc [OPTIONS]
Options
-a, --all: Include staged and unstaged changes-f, --force(alias--ignore-immutable): Override the immutability guard. By defaultgg screfuses to amend a commit whose PR is merged or which is already reachable fromorigin/<base>. See Core concepts · Immutable commits.
When unstaged changes are present, behavior is controlled by defaults.unstaged_action in .git/gg/config.json:
ask(default): prompt to stage all, stash, continue, or abortadd: auto-stage all changes (git add -A) and continuestash: auto-stash and continuecontinue: continue without including unstaged changesabort: fail immediately
Examples
# Standard amend-like flow
git add .
gg sc
# Include unstaged changes too
gg sc --all
gg absorb
Automatically distribute staged changes to the most appropriate commits in your stack.
gg absorb [OPTIONS]
Options
--dry-run: Preview actions without changing commits-a, --and-rebase: Rebase automatically after creating fixups-w, --whole-file: Match and absorb by whole file rather than hunks--one-fixup-per-commit: At most one fixup per commit-n, --no-limit: Search all commits in the stack (not just last 10)-s, --squash: Squash directly instead of creatingfixup!commits-f, --force(alias--ignore-immutable): Override the immutability guard. By default,gg absorbrefuses to run if any commit in the stack is merged or reachable fromorigin/<base>— because it cannot tell ahead of time whether git-absorb will target those commits.--dry-runskips the guard so you can preview safely. See Core concepts · Immutable commits.
Examples
# Preview before applying
gg absorb --dry-run
# Absorb and finish with rebase
gg absorb --and-rebase
# Heavy refactor across many files
gg absorb --whole-file --no-limit
gg drop
Remove one or more commits from the stack.
Alias: gg abandon
gg drop <TARGET>... [OPTIONS]
Arguments
<TARGET>...: One or more commits to drop. Each target can be:- Position (1-indexed):
1,3 - Short SHA:
abc1234 - GG-ID:
c-abc1234
- Position (1-indexed):
Options
-f, --force(alias--ignore-immutable): Skip the confirmation prompt and override the immutability guard.gg droprefuses by default to drop or rewrite commits whose PR is merged or which are already reachable fromorigin/<base>; this flag bypasses both safeties. See Core concepts · Immutable commits.--json: Output result as JSON
Behavior
- Validates the working directory is clean
- Resolves each target to a commit in the stack
- Shows which commits will be dropped and asks for confirmation (unless
--force) - Performs a
git rebase -ithat omits the dropped commits - Cleans up per-commit branches for dropped commits
- Prints a summary of what was dropped
At least one commit must remain in the stack after dropping.
Examples
# Drop the second commit in the stack
gg drop 2
# Drop multiple commits at once
gg drop 1 3
# Drop by GG-ID, skip confirmation
gg drop c-abc1234 --force
# Drop with JSON output
gg drop 2 --force --json
# Use the 'abandon' alias (inspired by jj)
gg abandon 2
JSON Output
{
"version": 1,
"drop": {
"dropped": [
{"position": 2, "sha": "abc1234", "title": "Fix typo"}
],
"remaining": 3
}
}
Edge Cases
- Dropping all commits produces an error — at least one commit must remain
- Invalid position shows the valid range
- Rebase conflicts are handled the same as
gg reorder— resolve withgg continueorgg abort
gg reorder / gg arrange
Reorder and/or drop commits in your stack.
gg arrange is an alias for gg reorder — they share the same implementation.
gg reorder [OPTIONS]
gg arrange [OPTIONS]
Options
-o, --order <ORDER>: New order as positions/SHAs ("3,1,2"or"3 1 2")--no-tui: Disable the interactive TUI and use a text editor instead-f, --force(alias--ignore-immutable): Override the immutability guard. Reordering or dropping a commit that is already merged or reachable fromorigin/<base>is refused by default. See Core concepts · Immutable commits.
Interactive TUI
When run without --order, opens an interactive TUI where you can visually rearrange and drop commits:
┌─ Arrange Stack ──────────────────────────────────┐
│ 1 abc1234 feat: add login page │
│▸ 2 def5678 fix: handle empty input ↕ │
│ [DROP] 3 ghi9012 refactor: extract validator │
│ 4 jkl3456 test: add integration tests │
└──────────────────────────────────────────────────┘
j/k:navigate J/K:move commit d:drop/undrop Enter/s:confirm q/Esc:cancel
Keyboard shortcuts
| Key | Action |
|---|---|
j / ↓ | Move cursor down |
k / ↑ | Move cursor up |
J / Shift+↓ | Move commit down |
K / Shift+↑ | Move commit up |
d / Delete | Toggle drop mark on commit |
Enter / s | Confirm new order |
q / Esc | Cancel (no changes) |
Position 1 is the bottom of the stack (closest to the base branch).
Dropped commits appear in red with strikethrough and a [DROP] prefix. You can still move dropped commits (e.g., to undrop them later). At least one commit must remain — you cannot drop all commits.
The TUI requires a TTY. In non-interactive environments (pipes, CI), gg reorder falls back to the text editor automatically. Use --no-tui to force the editor fallback.
Editor Fallback
When using the editor fallback (--no-tui or non-TTY), you can:
- Reorder commits by rearranging lines
- Drop commits by deleting their lines
At least one commit must remain.
Examples
# Interactive reorder/drop with TUI
gg reorder
gg arrange
# Explicit reorder by position (no dropping)
gg reorder --order "3,1,2"
# Use text editor instead of TUI
gg arrange --no-tui
gg split
Split a commit in the stack into two commits. The selected hunks become a new commit inserted before the original in the stack, while the remaining changes stay in the original commit.
gg split [OPTIONS] [FILES...]
Options
-c, --commit <TARGET>: Target commit — position (1-indexed), short SHA, or GG-ID. Defaults to the current commit (HEAD).-m, --message <MESSAGE>: Commit message for the new (first) commit. Skips the editor prompt.--no-edit: Keep the original message for the remainder commit without prompting.--no-tui: Disable TUI, use sequential prompt instead (legacygit add -pstyle).-f, --force(alias--ignore-immutable): Override the immutability guard. Splitting a merged or base-ancestor commit is refused by default. See Core concepts · Immutable commits.FILES...: Files to include in the new commit. When provided, all hunks from those files are auto-selected (skips the interactive picker).
How It Works
When you split commit K into two:
- New commit (K’) — Contains only the selected hunks. Inserted before K in the stack. Gets a new GG-ID.
- Remainder (K’’) — Contains the remaining hunks. Stays in K’s original position. Keeps the original GG-ID (preserving PR association).
All descendant commits are automatically rebased onto the remainder.
BEFORE AFTER
4: "Fix tests" 5: "Fix tests" (rebased)
3: "Add auth+logging" 4: "Add logging" ← remainder (keeps GG-ID)
2: "Setup DB" 3: "Add auth" ← NEW commit (selected changes)
1: "Init project" 2: "Setup DB"
1: "Init project"
Interactive Hunk Selection
Running gg split without file arguments opens the interactive hunk picker:
# Split the current commit — opens hunk selector
gg split
# Split a specific commit in the stack
gg split -c 3
TUI Mode (Default)
When run with a TTY, gg split opens a two-panel TUI for hunk selection:
┌── Files (1/3 width) ──┬── Diff (2/3 width) ──────────────┐
│ [✓] src/auth.rs (3) │ @@ -10,6 +10,12 @@ │
│ [ ] src/logging.rs (1)│ + // Validate token │
│ [~] src/tests.rs (2) │ + if token.is_empty() { │
│ │ + return false; │
│ │ + } │
├────────────────────────┴────────────────────────────────────┤
│ 5/12 hunks selected │ [Space] toggle · [Tab] switch panel │
└─────────────────────────────────────────────────────────────┘
TUI Keyboard Shortcuts
| Key | In File Panel | In Diff Panel |
|---|---|---|
| ↑/↓ or j/k | Navigate files | Navigate hunks |
| Space | Toggle all hunks for file | Toggle current hunk |
| a | Select all hunks (all files) | Select all hunks (this file) |
| n | Deselect all hunks (all files) | Deselect all hunks (this file) |
| s | — | Split current hunk into sub-hunks |
| Tab / ← / → | Switch to diff panel | Switch to file panel |
| Enter | Enter commit message | Enter commit message |
| q / Esc | Abort (cancel split) | Abort (cancel split) |
Inline Commit Message
After pressing Enter to confirm your hunk selection, an inline text input appears at the bottom of the TUI for the commit message. It’s pre-filled with Split from: <original commit title>.
| Key | Action |
|---|---|
| Enter | Confirm message and create the split |
| Esc | Go back to hunk selection |
| ← / → | Move cursor |
| Home / End | Jump to beginning/end |
| Backspace / Delete | Delete characters |
| Ctrl+A / Ctrl+E | Jump to beginning/end (emacs-style) |
| Ctrl+U | Clear from cursor to beginning |
| Ctrl+K | Clear from cursor to end |
After confirming the new commit message, a second inline input appears for the remainder commit message (pre-filled with the original commit’s message). This replaces the external editor for both messages, keeping the entire split workflow inside the TUI.
| Key | Action |
|---|---|
| Enter | Confirm remainder message and complete the split |
| Esc | Go back to the new commit message input |
The -m flag still works and bypasses the TUI input for the new commit. The --no-edit flag skips the remainder message input entirely, keeping the original message as-is.
File Panel Indicators
[✓]— All hunks selected (green)[~]— Some hunks selected (yellow)[ ]— No hunks selected
Sequential Prompt Mode (--no-tui)
Use --no-tui to fall back to the legacy git add -p style sequential prompt:
gg split --no-tui
This mode is automatically used when no TTY is available (e.g., in CI pipelines or when piping).
For each hunk, you’ll see the diff with colored output and a prompt:
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -10,6 +10,12 @@ fn authenticate(user: &str) -> bool {
+ // Validate token
+ if token.is_empty() {
+ return false;
+ }
Include this hunk? [y]es/[n]o/[a]ll file/[d]one file/[s]plit/[q]uit/?help:
Sequential Mode Actions
| Key | Action | Description |
|---|---|---|
y | Yes | Include this hunk in the new commit |
n | No | Skip this hunk (stays in remainder) |
a | All file | Include all remaining hunks from this file |
d | Done file | Skip all remaining hunks from this file |
s | Split | Split this hunk into smaller hunks |
q | Quit | Stop; all remaining hunks stay in remainder |
? | Help | Show this help |
File-Based Splitting
When file arguments are provided, all hunks from those files are auto-selected without opening the interactive picker:
# Move auth files to a new commit before the current one
gg split -m "Add authentication" src/auth.rs src/auth_test.rs
# Split a specific commit with explicit files
gg split -c 3 src/config.rs
# Non-interactive with both messages
gg split -c 2 -m "Extract helpers" --no-edit helpers.rs utils.rs
# Split by GG-ID
gg split -c c-abc1234 src/config.rs
Hunk Splitting
If a hunk contains multiple logical changes separated by unchanged lines, pressing s will break it into smaller hunks. You can then select each sub-hunk individually. If the hunk is already atomic (contiguous changes), you’ll see “This hunk cannot be split further.”
Edge Cases
- All changes selected — Warning: the original commit will be empty.
- No changes selected — Error.
- Dirty working directory — Error. Commit or stash changes first.
- Merge conflicts during rebase — Split is aborted, original state restored.
gg unstack
Split the current stack into two independent stacks.
gg unstack [--target <TARGET>] [--name <STACK_NAME>] [--no-tui] [-f] [--json] [-w]
The selected entry becomes the root of a new stack, and all entries above it move with it. Entries below the selected point remain in the original stack.
The command is called unstack because gg split already means splitting one
commit into two commits.
Examples
# Pick the split point interactively
gg unstack
# Move entries 3 and above into a new stack named auth-followup
gg unstack --target 3 --name auth-followup --no-tui
# Use a GG-ID or SHA prefix instead of a position
gg unstack --target c-abc1234 --name auth-followup --no-tui
# Machine-readable output
gg unstack --target 3 --json --no-tui
# Put the new upper stack in a managed worktree
gg unstack --target 3 --name upper-auth --wt
Behavior
Given a stack:
1 Add schema
2 Add API
3 Add UI
4 Add tests
Running gg unstack --target 3 --name ui-work leaves:
original stack: 1 Add schema, 2 Add API
new stack: 1 Add UI, 2 Add tests
GG-ID trailers are preserved. GG-Parent trailers are rewritten so the first
entry in each resulting stack has no parent and later entries point at the
previous entry in that same stack.
Local PR/MR mappings in .git/gg/config.json move with their entries. Stale
local entry branches under the old stack name are removed for moved entries.
If moved entries had review mappings, run gg sync afterwards to recreate or
update review branches for the new stack.
Without --worktree, the current directory switches to the new upper stack.
With --worktree, the current directory stays on the lower stack and the new
upper stack is checked out in its managed worktree.
Options
--target <TARGET>: first entry for the new stack. Accepts a 1-indexed position, GG-ID, or SHA prefix.--name <STACK_NAME>: name for the new stack. If omitted, gg generates a unique name such as<old-stack>-2.--no-tui: disable the interactive picker. Use this with--targetin scripts and tests.-f, --force: bypass the immutability guard for merged/base commits.--json: emit structured JSON.-w, --worktree: create or reuse a managed worktree for the new stack (--wtalso works).
Position 1 is rejected because it would leave the original stack empty. The
last position is allowed and creates a one-entry new stack.
gg rebase
Rebase the current stack onto an updated branch.
gg rebase [TARGET]
- If
TARGETis omitted, git-gud uses the stack base branch.
Options
-f, --force(alias--ignore-immutable): Override the immutability guard. Rebase rewrites the parent of every commit in the stack; merged commits (including squash-merged PRs) and commits already reachable fromorigin/<base>are silently skipped —git rebasedrops them automatically via patch-id matching, so--forceis not required for these. See Core concepts · Immutable commits.
Examples
# Rebase onto configured base
gg rebase
# Rebase onto specific branch
gg rebase main
gg land
Merge approved PRs/MRs from the bottom of your stack upward.
gg land [OPTIONS]
Options
-a, --all: Land all approved entries in sequence--auto-merge: (GitLab only) Request auto-merge instead of immediate merge--no-squash: Disable squash merge (squash is default)-w, --wait: Wait for CI and approvals before merging-u, --until <UNTIL>: Land up to a target entry (position, GG-ID, SHA)-c, --clean: Clean stack automatically after landing all--no-clean: Disable auto-clean for this run--admin: (GitHub only) Use admin privileges to bypass branch protection requirements (see Admin Override below)--json: Emit machine-readable JSON output (no human logs)
Examples
# Land one approved entry
gg land
# Land complete stack, waiting for readiness
gg land --all --wait
# Land part of stack
gg land --until 2
# GitLab auto-merge queue
gg land --all --auto-merge
# JSON output for automation
gg land --all --json
# Bypass approval requirements (GitHub admin)
gg land --admin
# Land full stack with admin override
gg land --all --wait --admin
Admin Override
The --admin flag (or land_admin config default) passes --admin to gh pr merge, which uses GitHub’s API-level admin merge. This bypasses all branch protection rules the merging user has permission to override, which may include both review approvals and required status checks depending on your repository settings.
Use --wait --admin if you want to wait for CI to pass before merging while still bypassing approval requirements. Without --wait, no client-side CI validation is performed.
On GitLab, --admin is a no-op — glab mr merge has no equivalent flag. A warning is printed and the merge proceeds normally.
A warning (⚠ Merging with admin override) is printed before each admin-elevated merge.
Downstream MR Retargeting
After landing an entry, gg land automatically retargets the next MR in the stack so it no longer points at the now-merged intermediate branch:
- Single entry (
gg land): retargets the immediate next MR tostack.base. - All entries (
gg land --all): retargets all remaining MRs tostack.baseas each entry is landed.
This applies to both GitHub PRs and GitLab MRs. No manual retargeting in the provider UI is needed after landing.
Merge Trains (GitLab)
When merge trains are enabled on the target branch, gg land automatically adds MRs to the merge train instead of merging directly.
Approval is always required before an MR can enter the merge train queue — even with --all. If using --wait, the command will show “Waiting for approval…” until a reviewer approves the MR.
CI Failure Details
When using --wait, if CI fails on an MR the command stops and shows which jobs failed:
OK Landed 1 MR(s)
⚠ Landed 1 MR(s), but encountered an error:
Error: MR !7621 CI failed
Failed jobs: lint (stage: test), build-android (stage: build)
This helps diagnose CI issues without having to open the GitLab UI. Failed job names and stages are fetched from the MR’s head pipeline.
JSON Output
Example JSON response:
{
"version": 1,
"land": {
"stack": "my-stack",
"base": "main",
"landed": [
{
"position": 1,
"sha": "abc1234",
"title": "feat: add parser",
"gg_id": "c-abc1234",
"pr_number": 42,
"action": "merged",
"error": null
}
],
"remaining": 0,
"cleaned": false,
"warnings": [],
"error": null
}
}
gg clean
Delete merged stacks (and associated managed worktrees).
gg clean [OPTIONS]
Options
-a, --all: Clean all merged stacks without prompting--json: Emit machine-readable JSON output
Examples
gg clean
gg clean --all
gg clean --json
--json prints:
version: output schema versionclean.cleaned: stacks that were cleanedclean.skipped: stacks skipped (unmerged or declined in interactive mode)
gg lint
Run configured lint commands on stack commits.
gg lint [OPTIONS]
Options
-u, --until <UNTIL>: Stop at target entry (position, GG-ID, SHA)--json: Emit structured JSON output
Examples
# Lint from bottom to current
gg lint
# Lint only a subset
gg lint --until 2
gg run
Run an arbitrary command on each commit in the stack. Like jj run in Jujutsu — useful for validating changes (build, test), applying auto-fixers (formatters, codemods), or any per-commit shell command.
gg run [OPTIONS] -- <COMMAND>...
Arguments after -- are passed through as-is. Use -- whenever your command has its own flags so clap knows where gg run’s options end.
Modes
gg run has three modes that control what happens to files the command modifies:
- Read-only (default): if the command modifies tracked files, the commit is marked failed. Ideal for build/test/lint validation.
--amend: stage and fold any modifications into the current commit, then rebase the rest of the stack on top. Ideal for formatters and codemods (cargo fmt,prettier,ruff --fix).--discard: revert any working-tree changes after each commit. Ideal for commands with known side effects you want to ignore.
Options
-u, --until <UNTIL>: stop at this commit position (default: current).--amend: fold file changes into each commit (see above).--discard: discard file changes after each commit.--keep-going: continue on command failure instead of stopping at the first failed commit (default is to stop).-j, --jobs <N>: number of parallel workers.0= auto-detect CPUs,1= sequential (default). Parallel mode only applies to read-only runs and uses isolated temporary worktrees, one per commit.--json: emit structured JSON output instead of human-readable text.
Examples
# Read-only validation across the whole stack
gg run -- cargo test
# Apply a formatter and fold changes into each commit
gg run --amend -- cargo fmt
# Run across up to commit 2 only
gg run --until 2 -- cargo check
# Parallel read-only run with 4 workers
gg run -j 4 -- cargo test
# Continue past failing commits (useful for auditing)
gg run --keep-going -- cargo clippy
# Pass a command with quoted arguments — argv boundaries are preserved
gg run -- sh -c 'echo "$GG_CURRENT_COMMIT" && cargo test --test integration'
JSON output
With --json, gg run emits a RunResponse:
{
"version": 1,
"run": {
"results": [
{
"position": 1,
"sha": "abc1234",
"title": "Add feature",
"passed": true,
"commands": [
{"command": "cargo test", "passed": true, "output": null}
]
}
],
"all_passed": true
}
}
The command field is a copy-pasteable shell form: arguments containing whitespace or special characters are single-quoted. It reflects your input, not the actual argv.
Notes
- Shell aliases (
alias gw=./gradlew) are not expanded. Use the real command path. .git/paths in the command (e.g..git/gg/lint.sh) are rewritten to the real git commondir so they resolve inside linked worktrees and inside parallel worker worktrees.- Parallel mode creates temporary worktrees under
$TMPDIR/gg-run-<pid>/, cleaned up automatically when the run finishes. - Under
--amend+ default stop-on-error, if a later commit fails, the already-amended commits above are preserved — the branch is not force-reset. Usegg continue/gg abortto resume if a rebase conflict happened instead.
gg setup
Interactive setup for .git/gg/config.json.
gg setup # Quick mode: essential settings only
gg setup --all # Full mode: all settings organized by category
Use this when:
- Starting git-gud in a new repository
- Working with self-hosted GitHub/GitLab
- Updating defaults (base branch, username, lint config)
Quick Mode (default)
Quick mode prompts for only the essential settings:
- Provider: GitHub or GitLab
- Base branch: Default base branch (main/master/trunk)
- Username: Username for branch naming
After completing quick setup, you’ll see:
Tip: Run 'gg setup --all' to configure advanced options (sync, land, lint, etc.)
Full Mode (--all)
Full mode organizes all settings into logical groups:
General
| Field | Type | Default | Description |
|---|---|---|---|
provider | select | auto-detect | GitHub or GitLab (auto-detects github.com and gitlab.com remotes; self-hosted instances require manual selection) |
base | string | auto-detect | Default base branch (main/master/trunk) |
branch_username | string | from CLI auth | Username for branch naming |
unstaged_action | select | ask | Action for gg amend with unstaged changes |
Sync
| Field | Type | Default | Description |
|---|---|---|---|
sync_auto_rebase | bool | false | Auto-rebase when base is behind origin |
sync_behind_threshold | number | 1 | Commits behind origin before warning/rebase |
sync_draft | bool | false | Create new PRs/MRs as drafts by default |
sync_update_descriptions | bool | true | Update PR/MR descriptions on re-sync |
sync_update_title | bool | false | Update PR/MR titles on re-sync |
stack_nav_comments | bool | false | Post a managed navigation comment linking all PRs/MRs in the stack |
Land
| Field | Type | Default | Description |
|---|---|---|---|
land_auto_clean | bool | false | Auto-clean stack after landing all |
land_wait_timeout_minutes | number | 30 | Timeout for gg land --wait |
Lint
| Field | Type | Default | Description |
|---|---|---|---|
lint | list | empty | Lint commands to run per commit |
sync_auto_lint | bool | false | Run lint automatically before sync |
Worktrees
| Field | Type | Default | Description |
|---|---|---|---|
worktree_base_path | string | empty | Template for stack worktrees ({repo}, {stack}) |
GitLab (only shown if provider is GitLab)
| Field | Type | Default | Description |
|---|---|---|---|
gitlab.auto_merge_on_land | bool | false | Use “merge when pipeline succeeds” |
Global Config
git-gud supports global configuration at ~/.config/gg/config.json. When running gg setup:
- If no local config exists, global defaults are shown in prompts
- Local config always takes precedence over global config
This allows you to set organization-wide defaults while allowing per-repo overrides.
All fields are written to config.json after setup, making it easy to review and edit configuration manually.
Note:
auto_add_gg_idsis deprecated. Existing configs that include it are still read, but setup no longer prompts for it and runtime behavior always treats it as enabled.
gg continue / gg abort
Control paused operations (typically rebases with conflicts).
gg continue
gg abort
Use gg continue after resolving conflicts and staging files.
Use gg abort when you want to stop and roll back the in-progress operation.
gg reconcile
Repair stack metadata when branches/PRs were manipulated outside gg sync.
gg reconcile [OPTIONS]
Options
-n, --dry-run: Preview only; make no changes
What it does
- Normalizes GG metadata trailers on stack commits (
GG-ID+GG-Parent) - Maps existing remote PRs/MRs back to local stack entries
Examples
# Safe preview
gg reconcile --dry-run
# Apply reconciliation
gg reconcile
gg restack
Repair stack ancestry after manual history changes (amend, cherry-pick, upstream rebase).
gg restack [OPTIONS]
Options
-n, --dry-run: Show what would be done without making changes--from <TARGET>: Repair only from this commit upward (position, SHA, or GG-ID)--json: Output result as JSON
Behavior
- Validates no rebase is already in progress
- Validates the working directory is clean
- Requires all commits to have GG-IDs (directs to
gg reconcileif missing) - Compares each entry’s
GG-Parenttrailer against the expected parent - If all parents match, reports “Stack is already consistent”
- If
--dry-run, displays the plan and exits - Performs a single
git rebase -ito realign the chain - Normalizes GG metadata after rebase
- Prints a summary with a hint to run
gg sync
Examples
# Check if the stack needs restacking (no changes)
gg restack --dry-run
# Repair the full stack
gg restack
# Repair only from position 3 upward
gg restack --from 3
# Repair from a specific GG-ID upward
gg restack --from c-abc1234
# Get JSON output
gg restack --json
# Combine dry-run and JSON
gg restack --dry-run --json
JSON Output
{
"version": 1,
"restack": {
"stack_name": "my-feature",
"total_entries": 4,
"entries_restacked": 2,
"entries_ok": 2,
"dry_run": false,
"steps": [
{
"position": 1,
"gg_id": "c-abc1234",
"title": "Add login form",
"action": "ok",
"current_parent": null,
"expected_parent": null
},
{
"position": 2,
"gg_id": "c-def5678",
"title": "Add validation",
"action": "reattach",
"current_parent": "c-old1111",
"expected_parent": "c-abc1234"
}
]
}
}
Edge Cases
- Empty stack produces an error
- Missing GG-IDs directs to
gg reconcile - Rebase in progress blocks with a clear error message
- Rebase conflicts are handled the same as
gg reorder— resolve withgg continueorgg abort --from 1is equivalent to a full restack
gg undo
Reverse the local ref/HEAD effects of the most recent mutating gg
command, backed by a per-repo operation log.
gg undo [OPERATION_ID] [--json]
gg undo --list [--limit N] [--json]
gg undo only moves refs and HEAD — it never modifies your working
tree, working-copy files, or the index. The log lives at
<commondir>/gg/operations/*.json and keeps the last 100 records;
Pending records (operations interrupted by a crash, Ctrl-C, or a
long-running conflict) are never pruned.
Options
OPERATION_ID: Target a specific record (op_…). When omitted, undoes the most recent locally-undoable operation.--list: Show recent operations with id, kind, status, timestamp, and undoability marker.--limit N: Cap--listoutput (default: 20).--json: Emit machine-readable JSON.
How it works
Every mutating gg command (sc, drop, split, unstack, rebase, reorder,
absorb, reconcile, checkout, mv/first/last/prev/next,
clean, sync, land, and run --amend) now snapshots the refs it
will touch before mutating and finalises the record on success. gg undo replays the refs_before snapshot of the target record, moving
refs back to where they were.
A second gg undo redoes the first — because undo is itself
recorded as an operation, running it twice reverses the reversal.
Entries created by gg undo appear in --list with a ↶ marker and
an undoes field pointing at the original operation id.
Refusal modes
gg undo refuses (exit 1, no refs touched) when:
| Reason | Condition | What to do |
|---|---|---|
remote | The target op pushed/merged/closed/created a PR or MR. | Use the printed provider hint (gh pr close <n>, glab mr close <n>, git push --delete …). Local state is unchanged. |
interrupted | The op crashed or was Ctrl-C’d mid-flight and has status: Interrupted. | Fix the underlying state manually; the stale Pending record is swept into Interrupted on the next lock-acquiring op. |
stale | Refs have moved since the target op finalised. | Run gg undo --list and target a more recent record instead. The error names the ref, the expected OID, and the actual OID. |
unsupported_schema | The record was written by a newer gg with a schema version this binary does not understand. | Upgrade gg or delete the offending record. |
Examples
# Reverse the last local operation
gg undo
# See what's on the log (newest first)
gg undo --list
gg undo --list --limit 5
# Target a specific record from --list
gg undo op_0000001750000000_018f…
# Redo: undo twice in a row
gg undo
gg undo
# Scripting
gg undo --list --json | jq '.operations[] | select(.is_undoable)'
JSON output
Schema versioning: all gg JSON responses share the top-level version
field (OUTPUT_VERSION). The undo types are additive — new optional
fields may be added in future releases without bumping the version.
Forward-compatible consumers should ignore unknown fields.
gg undo --json
{
"version": 1,
"status": "succeeded",
"undone": {
"id": "op_0000001750000000_018f…",
"kind": "drop",
"status": "committed",
"created_at_ms": 1750000000000,
"args": ["drop", "3"],
"stack_name": "feat/login",
"touched_remote": false,
"is_undoable": true,
"is_undo": false,
"remote_effects": []
}
}
On refusal:
{
"version": 1,
"status": "refused",
"refusal": {
"reason": "remote",
"message": "Cannot locally undo 'sync': it touched a remote.",
"target": { "id": "op_…", "kind": "sync", "touched_remote": true, "…": "…" },
"hints": [
"Close PR #42: gh pr close 42",
"Delete remote branch: git push --delete origin nacho/feat/1"
]
}
}
refusal.reason is one of remote, interrupted, stale,
unsupported_schema.
gg undo --list --json
{
"version": 1,
"operations": [
{
"id": "op_…",
"kind": "undo",
"status": "committed",
"created_at_ms": 1750000100000,
"args": ["undo"],
"stack_name": "feat/login",
"touched_remote": false,
"is_undoable": true,
"is_undo": true,
"undoes": "op_previous…"
},
{ "…": "…" }
]
}
Operations are returned newest-first. Use is_undoable to gate
UI/agent actions; use is_undo + undoes to render redo markers.
What gg undo does NOT do
- It does not restore working-tree files or the index. If you amended
a commit and want the old source back, use
git reflogorgit stash. - It does not touch remotes. Operations that pushed, merged, closed,
or created PRs/MRs are recorded (so you can see them in
--list) but refused for local replay. - It does not guarantee atomicity of the replay. If the process
dies mid-replay, a second
gg undowill finish the job — the working tree is clean throughout, so only refs move. - It does not support an
--all/--rangemode. Each call reverses exactly one operation.
See also
gg log— smartlog view of the current stack- MCP server —
stack_undoandstack_undo_listtools for agentic workflows
MCP Server
git-gud includes an MCP (Model Context Protocol) server that allows AI assistants like Claude Desktop, Cursor, and other MCP-compatible tools to interact with your stacked-diffs workflows programmatically.
Installation
The gg-mcp binary is distributed alongside gg. If you installed via Homebrew or cargo-dist, it should already be available.
Configuration
Claude Desktop
Add this to your Claude Desktop MCP configuration (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"git-gud": {
"command": "gg-mcp",
"env": {
"GG_REPO_PATH": "/path/to/your/repo"
}
}
}
}
Cursor / Other MCP Clients
Configure gg-mcp as a stdio-based MCP server. Set GG_REPO_PATH to point to your repository.
Environment Variables
| Variable | Description | Default |
|---|---|---|
GG_REPO_PATH | Path to the git repository | Current working directory |
Available Tools
stack_list
List the current stack with commit entries and PR/MR status.
Parameters:
refresh(boolean, optional): Refresh PR/MR status from remote before listing. Default:false.
Returns: Stack name, base branch, commit entries with positions, SHAs, titles, GG-IDs, PR numbers, states, CI status, and approval status.
stack_log
Render the current stack as a smartlog-style view (stack-scoped). Mirrors the CLI gg log --json output.
Parameters:
refresh(boolean, optional): Refresh PR/MR status from remote before rendering. Default:false.
Returns: { stack, base, current_position, entries: [...] }. Entry fields match stack_list. Use stack_list_all when you need a cross-stack overview.
stack_list_all
List all stacks in the repository with summary information.
Parameters: None.
Returns: Current stack name and a list of all stacks with name, base branch, commit count, and whether each is the current stack.
stack_inbox
Show actionable triage across local stacks. Mirrors gg inbox --json and groups PRs/MRs into action buckets like ready to land, blocked on CI, awaiting review, behind base, draft, and optionally merged.
Parameters:
all(boolean, optional): Include merged items as well.
Returns: A versioned JSON payload with total_items and buckets, where each bucket contains entries with stack name, position, SHA, title, PR/MR number, URL, CI status, and optional behind-base count.
stack_status
Get a quick status summary of the current stack.
Parameters: None.
Returns: Stack name, base branch, total commits, synced commits, current position, and how many commits behind the base branch.
pr_info
Get detailed information about a specific PR/MR by number.
Parameters:
number(integer, required): The PR/MR number to look up.
Returns: PR number, title, state (open/merged/closed/draft), URL, draft status, approval status, mergeability, and CI status.
config_show
Show the current git-gud configuration for this repository.
Parameters: None.
Returns: Provider, base branch, branch username, lint commands, and boolean settings (including compatibility field auto_add_gg_ids, which is always returned as true).
Write Tools
stack_checkout
Create a new stack or switch to an existing one.
Parameters:
name(string, optional): Stack name.base(string, optional): Base branch (default: main/master).worktree(boolean, optional): Use a git worktree for isolation.
stack_sync
Push branches and create/update PRs/MRs for the current stack.
Parameters:
draft(boolean, optional): Create PRs as draft.force(boolean, optional): Force-push branches.update_descriptions(boolean, optional): Update PR descriptions from commit messages.update_title(boolean, optional): Update PR titles from commit messages.no_rebase_check(boolean, optional): Skip rebase-needed check.lint(boolean, optional): Run lint before syncing.until(string, optional): Only sync up to this position/GG-ID/SHA.
stack_land
Merge approved PRs/MRs from the current stack.
Parameters:
all(boolean, optional): Land all approved PRs.squash(boolean, optional): Use squash merge.auto_clean(boolean, optional): Auto-clean the stack after landing.until(string, optional): Only land up to this position/GG-ID/SHA.
stack_clean
Clean up stacks whose PRs have been merged.
Parameters:
all(boolean, optional): Clean all merged stacks.
stack_rebase
Rebase the current stack onto the latest base branch.
Parameters:
target(string, optional): Target branch to rebase onto.
stack_squash
Squash (amend) staged changes into the current commit.
Parameters:
all(boolean, optional): Stage all changes first.
stack_absorb
Auto-absorb staged changes into the correct commits.
Parameters:
dry_run(boolean, optional): Show what would be absorbed.and_rebase(boolean, optional): Rebase after absorbing.whole_file(boolean, optional): Absorb whole files.one_fixup_per_commit(boolean, optional): One fixup per target commit.squash(boolean, optional): Squash fixups immediately.
stack_reconcile
Reconcile out-of-sync branches pushed outside of gg.
Parameters:
dry_run(boolean, optional): Show what would change.
stack_move
Move to a specific commit in the stack.
Parameters:
target(string, required): Position number, GG-ID, or SHA prefix.
stack_navigate
Navigate within the stack.
Parameters:
direction(string, required):"first","last","prev", or"next".
stack_lint
Run configured lint commands on each commit.
Parameters:
until(integer, optional): Only lint up to this position.
stack_drop
Remove commits from the stack.
Parameters:
targets(array of strings, required): Commits to drop—positions (1-indexed), short SHAs, or GG-IDs.
Notes: Always uses --force (the agent is expected to confirm with the user before calling). Returns JSON with dropped commits.
stack_split
Split a commit into two by moving specified files to a new commit.
Parameters:
commit(string, optional): Target commit—position (1-indexed), short SHA, or GG-ID. Defaults to the current commit.files(array of strings, required): Files to include in the new commit.message(string, optional): Message for the new (first) commit.no_edit(boolean, optional): Don’t prompt for the remainder commit message.
Notes: File-level only (no hunk selection via MCP). The new commit is inserted before the original.
stack_reorder
Reorder commits in the stack with an explicit order.
Parameters:
order(string, required): New order as positions (1-indexed), e.g.,"3,1,2"or"3 1 2".
Notes: No TUI via MCP. The order specifies the new bottom-to-top arrangement of commits.
stack_undo
Reverse the local ref/HEAD effects of the most recent mutating gg
command (or a specific operation by id). Shell-out wrapper around
gg undo --json.
Parameters:
operation_id(string, optional): Target a specific record (seestack_undo_list). Defaults to the most-recent-undoable operation.
Notes: Refuses on remote-touching operations (sync, land) —
the returned payload includes a refusal.reason of remote plus a
provider-specific revert hint (e.g. gh pr close <n>, git push --delete …). Agents must surface the hint to the user rather than
attempt silent remote rollback. Also refuses on interrupted,
stale, and unsupported_schema records; the working tree is never
modified.
stack_undo_list
List recent operations from the per-repo operation log (newest-first).
Shell-out wrapper around gg undo --list --json.
Parameters:
limit(integer, optional): Cap the number of records returned (default: 20).
Notes: Each entry carries is_undoable (gate for safe local
replay) and, for undo records, is_undo + undoes (the operation id
being reversed). Remote-touching ops appear with is_undoable: false
and touched_remote: true.
Transport
The MCP server uses stdio transport (JSON-RPC over stdin/stdout), which is the standard for local MCP tools. No network configuration is needed.
Configuration
git-gud uses a layered configuration system:
- Global config:
~/.config/gg/config.json— shared defaults across all repos - Local config:
.git/gg/config.json— per-repository settings
Local config always takes precedence over global config.
Setup
Initialize or update local config with:
gg setup # Quick mode: essential settings only
gg setup --all # Full mode: all settings organized by category
For global config, manually create ~/.config/gg/config.json with your preferred defaults.
Example config
{
"defaults": {
"provider": "gitlab",
"base": "main",
"branch_username": "your-username",
"lint": [
"cargo fmt --check",
"cargo clippy -- -D warnings"
],
"auto_add_gg_ids": true,
"unstaged_action": "ask",
"land_wait_timeout_minutes": 30,
"land_admin": false,
"land_auto_clean": false,
"sync_auto_lint": false,
"sync_auto_rebase": false,
"sync_behind_threshold": 1,
"sync_draft": false,
"sync_update_descriptions": true,
"sync_update_title": false,
"worktree_base_path": "/tmp/gg-worktrees",
"gitlab": {
"auto_merge_on_land": false
}
}
}
defaults options
| Option | Type | What it controls | Default |
|---|---|---|---|
provider | string | Provider (github/gitlab) for self-hosted or explicit override | Auto-detected |
base | string | Default base branch for new stacks | Auto-detected |
branch_username | string | Username prefix in stack/entry branch names | Auto-detected |
lint | string[] | Commands used by gg lint / gg sync --lint | [] |
auto_add_gg_ids | boolean | Deprecated compatibility field. gg always enforces GG metadata normalization, regardless of this value. | true |
unstaged_action | string | Default behavior for gg sc/gg amend when unstaged changes exist: ask, add, stash, continue, or abort | ask |
land_wait_timeout_minutes | number | Timeout for gg land --wait polling | 30 |
land_admin | boolean | Use admin privileges to bypass approval requirements on land (GitHub only) | false |
land_auto_clean | boolean | Auto-run cleanup after full landing | false |
sync_auto_lint | boolean | Automatically run gg lint before gg sync | false |
sync_auto_rebase | boolean | Automatically run gg rebase before gg sync when behind threshold is reached | false |
sync_behind_threshold | number | Warn/rebase in gg sync when base is at least this many commits behind origin/<base> (0 disables check) | 1 |
sync_draft | boolean | Create new PRs/MRs as drafts by default | false |
sync_update_descriptions | boolean | Update PR/MR descriptions on re-sync | true |
sync_update_title | boolean | Update PR/MR titles on re-sync | false |
stack_nav_comments | boolean | Post a managed navigation comment on each open PR/MR in a multi-entry stack, listing all entries with a 👉 marker on the current one. When set back to false, the next gg sync removes any previously-posted managed comments. Skipped for single-entry stacks and when --until limits a sync. | false |
worktree_base_path | string | Base directory for managed worktrees | Parent of repo |
gitlab.auto_merge_on_land | boolean | Default GitLab auto-merge behavior for gg land | false |
Global Config
Store shared defaults in ~/.config/gg/config.json. This is useful for:
- Organization-wide settings (e.g., always use drafts)
- Personal preferences that apply to all your repos
- Reducing repetitive setup across multiple repositories
Example global config:
{
"defaults": {
"sync_draft": true,
"land_auto_clean": true,
"sync_behind_threshold": 5
}
}
When gg setup runs in a new repo, these global defaults will be shown in prompts. You can accept them or override per-repo.
Stack state
git-gud also stores stack-specific state in the local config file (for example PR/MR mappings by GG-ID). This is how it remembers which commit corresponds to which PR/MR over time.
PR/MR templates
You can customize descriptions by creating .git/gg/pr_template.md.
Supported placeholders:
{{title}}{{description}}{{stack_name}}{{commit_sha}}
Example:
## Summary
{{description}}
---
**Stack:** `{{stack_name}}`
**Commit:** `{{commit_sha}}`
Shell Completions
Generate completions with:
gg completions <shell>
Supported shells include: bash, zsh, fish, elvish, powershell.
Bash
mkdir -p ~/.local/share/bash-completion/completions
gg completions bash > ~/.local/share/bash-completion/completions/gg
Zsh
mkdir -p ~/.zfunc
gg completions zsh > ~/.zfunc/_gg
Then in ~/.zshrc:
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
Fish
mkdir -p ~/.config/fish/completions
gg completions fish > ~/.config/fish/completions/gg.fish
Troubleshooting / FAQ
gh or glab is missing
Install the provider CLI:
- GitHub: https://cli.github.com/
- GitLab: https://gitlab.com/gitlab-org/cli
Not authenticated with provider
gh auth login
glab auth login
“Not on a stack branch”
You’re on a branch that doesn’t match the stack naming scheme.
gg co <stack-name>
I pushed with git push and now mappings are wrong
Run reconcile:
gg reconcile --dry-run
gg reconcile
Merge commits are not supported
Stacks require linear history. Rebase your branch:
git rebase main
gg land --wait times out
Increase timeout in config:
{
"defaults": {
"land_wait_timeout_minutes": 60
}
}
When should I use gg absorb vs gg sc?
- Use
gg scwhen you’re on the exact commit you want to modify. - Use
gg absorbwhen staged edits belong to multiple commits and you want git-gud to distribute them.
How do I undo a gg command?
Run gg undo. It reverses the local ref/HEAD
effects of the most recent mutating gg command (drop, squash, split, unstack,
rebase, reorder, absorb, reconcile, checkout, nav, clean, sync, land,
or run --amend). Working-tree changes are not touched.
gg undo # reverse the last local operation
gg undo --list # see the recent operation log
gg undo <op_id> # target a specific record
gg undo; gg undo # undo then redo — a second undo reverses the first
Remote-touching operations (sync, land) are recorded but refused
for local replay. gg undo prints a provider-specific revert hint
(e.g. gh pr close <n>, git push --delete …) instead of silently
rewriting published history. See gg undo for
the full refusal matrix and JSON schema.