Commands
Every CLI command, what it does, and what it does to the stack. Each section shows a before/after diagram so you can predict the effect.
giff init
Creates the global config file at ~/.config/giff/config.toml if it doesn't exist. Idempotent.
giff init
The skeleton it writes:
[github] token = "" base_url = "https://api.github.com" [defaults] trunk = "main" draft_prs = true pr_template = ""
Edit the file or set GITHUB_TOKEN in your shell. Env wins.
giff new <branch>
Creates a new git branch from the current commit, checks it out, and registers it as a frame in the current stack (or starts a new stack if you're on the trunk).
giff new feat/auth-tokens
flowchart LR
subgraph BEFORE[ before ]
direction BT
main1(["main"])
base1["<b>feat/auth-base</b><br/><span style='color:#86868b;font-size:11px'>← here</span>"]:::brand
main1 --> base1
end
subgraph AFTER[ after ]
direction BT
main2(["main"])
base2["feat/auth-base"]
tokens["<b>feat/auth-tokens</b><br/><span style='color:#86868b;font-size:11px'>← here · 0 commits</span>"]:::brand
main2 --> base2 --> tokens
end
classDef brand fill:#ff0035,stroke:#ff0035,color:#ffffff;The new frame inherits its parent from whatever you were on. If you were on main, the frame becomes a stack root.
giff publish <message>
The recommended way to add a frame: creates the branch and commits the staged changes in one step. The message is used both as the commit message and (slugified) as the branch name.
git add . giff publish "feat: add token signing" # → branch: feat/token-signing (conventional prefix becomes a path segment) # → commit: "feat: add token signing"
Override the auto-derived branch:
giff publish "Some long descriptive message" -b feat/short-name
Auto-stage all tracked changes (like git commit -a):
giff publish -a "Quick patch"
giff commit -m "msg"
The first commit on a frame. Refuses if the frame already has a commit.
giff commit -m "scaffold auth" # creates the one commit on this frame giff commit --amend -m "scaffold auth (revised)" # always allowed — preserves the invariant giff commit --amend # amend without changing the message giff commit -a -m "auto-stage tracked changes"
If you try a second commit, you get:
error: frame `feat/auth-base` already has 1 commit(s) ahead of `main` —
one commit per frame is enforced.
Options:
• Start a new frame on top: giff new <branch-name>
• Amend the existing commit: giff commit --amend [-m "<message>"]giff checkout <target>
Move to a frame by branch name or by 1-based position.
giff checkout feat/auth-tokens # by name giff checkout 2 # by position — only valid in linear stacks
Position-based checkout refuses on tree-shaped stacks (positions are ambiguous when there are siblings); use names there.
giff next / giff prev
Move one frame up (next) or one frame down (prev) along the current branch's chain.
giff next # checkout the child frame giff prev # checkout the parent frame
If the current frame has multiple children, giff next opens an interactive picker:
╭ frame `feat/root` has 2 children — pick one ──────────╮ │ ▸ feat/branch-a #43 │ │ feat/branch-b #44 │ ╰────────────────────────────────────────────────────────╯ ↑↓ move Enter checkout Esc cancel
giff status
Where am I.
giff status
branch: feat/auth-tokens stack: auth-refactor (3 frames, linear) path: main → feat/auth-base → feat/auth-tokens depth: 1 (children: 0) PR: #43
giff dashboard
Open the web dashboard in your default browser. Starts an embedded HTTP server on a localhost port and serves the SvelteKit app baked into the giff binary.
giff dashboard
giff dashboard listening on: → http://local.giffstack.com:51743 (preferred — branded URL via DNS to 127.0.0.1) → http://localhost:51743 (fallback if your DNS blocks the above) opening browser… press Ctrl-C to stop
Same UI as giffstack.com, your token in localStorage, no data leaves your machine. Ctrl-C in the terminal when you're done.
giff log
Tree view of the current repo's stacks. By default hides frames whose PR is closed or merged.
giff log # only frames with open or no PR giff log --all # include closed/merged
stack: auth-refactor (trunk: main) ● main │ ◉ feat/auth-base [PR #42 [open]] │ ◉ feat/auth-tokens [PR #43 [open]] ← you are here │ ◉ feat/auth-middleware [PR #44 [open]]
For trees, the connectors fork:
stack: y-stack (trunk: main) ● main │ ◉ feat/root [PR #1 [open]] │ ├─ ◉ feat/left [PR #2 [open]] │ └─ ◉ feat/right [PR #3 [open]]
giff push
Opens or updates a PR for every frame in the current stack.
giff push
What happens, in order:
- Validates the stack (no cycles, no orphans, no duplicate branches).
- Pushes every branch to
originin a singlegit push --force-with-leasewith multiple refspecs — one SSH handshake for the whole stack. - For each frame in topological order, in parallel (8 workers): creates a new PR if there's no
pr_numberyet, otherwise updates the existing PR's body and base. Each PR'sbaseis its parent's branch (or the trunk for roots). - Embeds the
giffJSON metadata block at the end of every PR description. - Writes new
pr_numbers back to.git/stacked.toml.
giff sync
Pulls trunk, reconciles merged PRs, rebases the whole stack onto fresh trunk.
giff sync
The full flow:
- Reconcile. Asks GitHub for the status of every tracked PR (in parallel). For each merged frame: removes it from the local store and retargets every child PR's base on GitHub (walking past consecutive merged ancestors).
- Pull trunk.
git fetch origin <trunk>+git rebase origin/<trunk> <trunk>. - Restack. Each remaining frame is rebased onto its parent in topological order.
flowchart LR
subgraph BEFORE[ before sync ]
direction BT
m1(["main<br/>(old)"])
b1["<b>feat/auth-base</b><br/><span style='color:#86868b;font-size:11px'>PR #42 · MERGED on github</span>"]:::merged
t1["feat/auth-tokens<br/><span style='color:#86868b;font-size:11px'>PR #43 → base</span>"]
mw1["feat/auth-mw<br/><span style='color:#86868b;font-size:11px'>PR #44 → tokens</span>"]
m1 --> b1 --> t1 --> mw1
end
subgraph AFTER[ after sync ]
direction BT
m2(["main<br/><span style='color:#86868b;font-size:11px'>now includes auth-base</span>"])
t2["feat/auth-tokens<br/><span style='color:#86868b;font-size:11px'>PR #43 → main (retargeted)</span>"]:::brand
mw2["feat/auth-mw<br/><span style='color:#86868b;font-size:11px'>PR #44 → tokens</span>"]
m2 --> t2 --> mw2
end
classDef merged fill:#e5e5ea,stroke:#86868b,color:#86868b;
classDef brand fill:#ff0035,stroke:#ff0035,color:#ffffff;Conflicts
If a frame conflicts during the rebase, sync stops:
[2/3] Rebasing feat/auth-tokens onto feat/auth-base... conflict in feat/auth-tokens Resolve the conflicts, stage your changes, then run: git rebase --continue giff sync --continue
State is saved in .git/giff_sync_resume.json. Resolve normally with git, then resume with:
giff sync --continue
giff stack reorder
Interactive TUI for re-arranging frames in a linear stack. Linear stacks only — for trees, restructure with drop / squash.
giff stack reorder
↑↓ to move the highlighted frame, Enter to apply, Esc to cancel. Run giff push afterward to update PRs.
giff stack squash <branch>
Merges a frame's commit into its parent's commit. Preserves the one-commit-per-frame invariant by amending the parent's commit, not adding on top.
giff stack squash feat/auth-tokens
flowchart LR
subgraph BEFORE[ before ]
direction BT
m1(["main"])
b1["feat/auth-base<br/><span style='color:#86868b;font-size:11px'>1 commit</span>"]
t1["<b>feat/auth-tokens</b><br/><span style='color:#86868b;font-size:11px'>squashing</span>"]:::brand
mw1["feat/auth-mw<br/><span style='color:#86868b;font-size:11px'>1 commit</span>"]
m1 --> b1 --> t1 --> mw1
end
subgraph AFTER[ after ]
direction BT
m2(["main"])
b2["<b>feat/auth-base</b><br/><span style='color:#86868b;font-size:11px'>1 commit · contains both diffs</span>"]:::brand
mw2["feat/auth-mw<br/><span style='color:#86868b;font-size:11px'>re-parented to base</span>"]
m2 --> b2 --> mw2
end
classDef brand fill:#ff0035,stroke:#ff0035,color:#ffffff;Refuses if the parent has no commits to squash into, or if the child has no commits to squash. Children of the squashed frame are re-parented to the squash target.
giff stack drop <branch>
Removes a frame from the stack. Children are re-parented to the dropped frame's parent (so a Y-shape becomes two roots when you drop the root).
giff stack drop feat/auth-tokens
flowchart LR
subgraph BEFORE[ before ]
direction BT
m1(["main"])
b1["feat/auth-base"]
t1["<b>feat/auth-tokens</b><br/><span style='color:#86868b;font-size:11px'>dropping</span>"]:::brand
mw1["feat/auth-mw"]
m1 --> b1 --> t1 --> mw1
end
subgraph AFTER[ after ]
direction BT
m2(["main"])
b2["feat/auth-base"]
mw2["<b>feat/auth-mw</b><br/><span style='color:#86868b;font-size:11px'>re-parented to base</span>"]:::brand
m2 --> b2 --> mw2
end
classDef brand fill:#ff0035,stroke:#ff0035,color:#ffffff;giff stack land [--method merge|squash|rebase]
Merges the bottom (root) PR via the GitHub API and promotes the rest of the stack down.
giff stack land # default: merge commit giff stack land --method squash # squash-merge giff stack land --method rebase # rebase-merge
Steps:
- Refuses if the stack has multiple roots (no unique "bottom" to land).
- Calls GitHub's merge API on the root PR with the chosen method.
- Removes the root frame from the local store.
- For every direct child of the landed frame: sets
parent = Noneand updates its PR'sbaseto the trunk on GitHub.
giff parent-branch (internal)
Hidden subcommand. Prints the parent branch of the current frame. Used internally by the pre-commit hook. You shouldn't need to run it directly.
giff parent-branch # → "main" or whatever the parent's branch is