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.

bash
giff init

The skeleton it writes:

toml
[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).

bash
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;
Effect of `giff new feat/auth-tokens` while on `feat/auth-base`.

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.

bash
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:

bash
giff publish "Some long descriptive message" -b feat/short-name

Auto-stage all tracked changes (like git commit -a):

bash
giff publish -a "Quick patch"

giff commit -m "msg"

The first commit on a frame. Refuses if the frame already has a commit.

bash
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.

bash
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.

bash
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.

bash
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.

bash
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.

bash
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.

bash
giff push

What happens, in order:

  1. Validates the stack (no cycles, no orphans, no duplicate branches).
  2. Pushes every branch to origin in a single git push --force-with-lease with multiple refspecs — one SSH handshake for the whole stack.
  3. For each frame in topological order, in parallel (8 workers): creates a new PR if there's no pr_number yet, otherwise updates the existing PR's body and base. Each PR's base is its parent's branch (or the trunk for roots).
  4. Embeds the giff JSON metadata block at the end of every PR description.
  5. Writes new pr_numbers back to .git/stacked.toml.

giff sync

Pulls trunk, reconciles merged PRs, rebases the whole stack onto fresh trunk.

bash
giff sync

The full flow:

  1. 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).
  2. Pull trunk. git fetch origin <trunk> + git rebase origin/<trunk> <trunk>.
  3. 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;
`giff sync` after the bottom PR was merged via GitHub web UI.

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:

bash
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.

bash
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.

bash
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;
Squash: feat/auth-tokens' changes get folded into feat/auth-base's commit.

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).

bash
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;
Dropping the middle frame of a chain: above is re-parented down.

giff stack land [--method merge|squash|rebase]

Merges the bottom (root) PR via the GitHub API and promotes the rest of the stack down.

bash
giff stack land                  # default: merge commit
giff stack land --method squash  # squash-merge
giff stack land --method rebase  # rebase-merge

Steps:

  1. Refuses if the stack has multiple roots (no unique "bottom" to land).
  2. Calls GitHub's merge API on the root PR with the chosen method.
  3. Removes the root frame from the local store.
  4. For every direct child of the landed frame: sets parent = None and updates its PR's base to 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.

bash
giff parent-branch    # → "main" or whatever the parent's branch is
giff stack · open source · MIT Source