# Smart Rebase

> 現在の作業ブランチを origin/main などのベースブランチに rebase し、コンフリクトを解決して --force-with-lease で force push する。Use this skill whenever the user asks to rebase or sync/update/refresh their branch against main — "リベースして", "smart rebase", "/smart-rebase", "mainを取り込んで", "main の最新に追いつかせて", "rebase して force push", "最新mainに乗せて", "PRが遅れてる/behind", "rebase onto main" — even if they don't spell out every step. Also use it when a PR shows conflicts with its base branch and the fix is to rebase. Prefers existing GitHub tooling when it applies — `gh pr update-branch --rebase` for a server-side rebase with no local force-push, and `gh stack sync` on stacked branches — so also use it for "update branch", "PRのブランチを更新", "スタックを最新にして", "stack sync", "スタックPRをrebase", or updating a chain of stacked PRs against main.

- Skill: `tonkotsuboy/smart-rebase` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tonkotsuboy/smart-rebase`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tonkotsuboy/smart-rebase/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tonkotsuboy (https://skillmd.com/u/tonkotsuboy)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tonkotsuboy/smart-rebase

---


# smart-rebase — rebase onto the base branch and force-push

The whole workflow is in `scripts/rebase-sync.sh`, split into subcommands so a
conflict can stop the run, get fixed, and resume. Drive the script — don't
hand-roll `git fetch`/`rebase`/`push` calls, because the script also carries the
safety checks (refuse on the base branch, lease-protected push, autostash).

`$SKILL_DIR` below is this skill's directory.

## Three paths, decided by `preflight`

The guiding principle: **prefer the tool that already knows how to do this
safely over a hand-rolled sequence.** `gh` covers two situations better than
local git can — a chain of stacked PRs, and a plain PR that just needs catching
up — so reach for those first and fall back to local git only when neither fits.

`preflight` prints a `STACK:` line and a `SERVER:` line, and together they route
everything that follows:

| preflight says | Path | Why it wins |
|---|---|---|
| `STACK: stack` | **Stack path** — `stack-sync` | Cascade-rebases the whole chain |
| `SERVER: eligible` | **Server path** — `server-sync` | GitHub rebases; no local force-push |
| otherwise | **Plain path** — `rebase` → `push` | Handles everything else, incl. conflicts |

Check `STACK:` first — it outranks `SERVER:`, because updating one PR branch of a
stack in isolation strands the layers above it.

Also read `STATUS:` before doing any work. `up-to-date` means the base is already
contained in this branch and nothing needs to happen — say so and stop rather
than performing a no-op rebase. `push-only` means there is nothing to rebase but
local and remote differ, so only step 5 (`push`) is needed.

### Why the stack path exists

A stack is a chain where each branch's parent is the branch below it, so
rebasing one layer with plain git leaves the layers above pointing at the old
parent — their PR diffs then show the lower layer's commits as if they were new
work, and reviewers see noise that isn't theirs. `gh stack sync`
cascade-rebases every layer onto its updated parent and pushes them atomically,
which is why it wins whenever it applies.

### Why the server path exists

`gh pr update-branch --rebase` asks GitHub to rebase the PR's head branch onto
its base and rewrite the remote ref server-side. That beats a local rebase when
it applies: one round trip instead of fetch/rebase/push, GitHub validates the
expected head and refuses atomically on conflict, and **nothing is force-pushed
from here** — so the classic failure mode of clobbering someone else's commits
can't arise.

The trade is a reversal of trust: afterwards the rewritten remote is the source
of truth and `server-sync` hard-resets the local branch onto it. That is only
sound when the local ref is genuinely disposable, so the script requires an open
same-repo PR whose base matches, `HEAD` identical to the PR's remote head, and a
clean tree. Any other situation reports a reason on the `SERVER:` line and you
take the plain path — including the case where `gh` itself is unavailable.

Two things to keep in mind when reporting back. The rewritten commits are created
by GitHub, so the author is preserved but the **committer** becomes the GitHub
account — which can differ from the local git config, and means a locally-created
signature does not survive. And GitHub **drops commits that become empty** against
the new base; that is usually correct, but the script flags it as a
`COMMITS_BEFORE`/`AFTER` mismatch precisely because it is the one way this path
changes history without saying so.

## Stack path

1. Confirm with the user **first**. `gh stack sync` fetches, rebases and pushes in
   one indivisible step, so there is no post-rebase/pre-push checkpoint to stop at
   the way the plain path has. Tell them it will force-push (with lease) every
   branch in the stack, and show the stack from `preflight`'s output.

2. ```bash
   bash "$SKILL_DIR/scripts/rebase-sync.sh" stack-sync
   ```

   `STATUS: synced` and you're done — report which layers moved.

3. **Exit 10 means conflicts.** `sync` is conservative here: it rolls every branch
   back to its original state and pushes nothing, so the stack is untouched. Replay
   it with pauses instead:

   ```bash
   bash "$SKILL_DIR/scripts/rebase-sync.sh" stack-rebase
   ```

   Resolve conflicts exactly as in the plain path below (same inverted
   ours/theirs caveat), `git add` each file, then `stack-continue` — repeating
   while it keeps exiting 10, since each layer is replayed in turn. `stack-abort`
   restores every branch. Once it reports `STATUS: rebased`, run `stack-sync`
   again to push.

4. `STATUS: sync-failed` usually means the local and remote stacks diverged.
   `sync` needs an interactive terminal to ask which side wins, and it aborts
   rather than guess — so nothing was changed. Don't try to force it: report the
   message and ask the user to run `gh stack sync` in their own terminal.

## Server path

Used when `STACK:` is not `stack` and `SERVER: eligible`.

1. Confirm with the user **first**, for the same reason as the stack path: rebase
   and remote-rewrite happen in one indivisible call, so there is no
   post-rebase/pre-push checkpoint. Tell them GitHub will rewrite
   `origin/<branch>` and that their local branch will be reset onto the result.
   Skip this only if they already said to push without asking.

2. ```bash
   bash "$SKILL_DIR/scripts/rebase-sync.sh" server-sync
   ```

   `STATUS: synced` means GitHub rewrote the branch and the local branch now
   matches it. Report the new commits, and call out a `COMMITS_BEFORE`/`AFTER`
   mismatch if there is one — that means commits were dropped as empty, which is
   usually correct but is exactly the kind of thing the user should confirm.

3. **Exit 10 (`STATUS: server-conflict`) means conflicts.** GitHub refused and
   changed nothing. Conflicts can only be resolved where the files can be read
   and edited, so fall through to the plain path below — start at its step 2.

4. `STATUS: server-failed` or `server-timeout` — nothing was changed either way.
   `server-timeout` means GitHub accepted the request but the ref wasn't rewritten
   within ~30s. Don't retry `server-sync` blindly; run `preflight` again to see
   the current state, and if it's still behind, take the plain path.

Everything below is the **plain path**, used when `STACK:` is not `stack` and the
server path doesn't apply or bailed out.

## Why it's split into subcommands

`git rebase` stores its paused state in `.git/rebase-merge/`, which outlives the
process. So stopping at a conflict and resuming from a later shell invocation is
safe — that's what makes `rebase` → (fix) → `continue` → `push` work.

## Steps

1. **Preflight** — always first. It fetches the base branch and reports the
   branch, the resolved base, whether the tree is dirty, whether the branch is
   actually behind, and which path applies:

   ```bash
   bash "$SKILL_DIR/scripts/rebase-sync.sh" preflight
   ```

   The base branch is auto-detected from `origin/HEAD`, falling back to
   `main`/`master`/`develop`. If the user names a different base, pass it as an
   argument to `preflight` and `rebase` (e.g. `... rebase develop`).

   If it exits non-zero because the current branch *is* the base branch, stop and
   tell the user — rebasing and force-pushing `main` itself would rewrite shared
   history. Ask which feature branch they meant.

   Then route on the output before doing anything else:
   `STATUS: ready-stacked` → **stack path**; `SERVER: eligible` → **server path**;
   `STATUS: up-to-date` → nothing to do, say so and stop; `STATUS: push-only` →
   skip to step 5. Otherwise continue here.

2. **Rebase**:

   ```bash
   bash "$SKILL_DIR/scripts/rebase-sync.sh" rebase
   ```

   Uncommitted work is stashed automatically (`--autostash`) and restored by git
   as soon as the rebase finishes, so you don't need to manage the stash.

3. **If exit code is 10, there are conflicts.** The script prints the conflicted
   files and leaves the rebase in progress. Resolve them yourself:

   - Read each conflicted file and understand *both* sides. The user's own commits
     are being replayed on top of the base, so "ours" is the base branch's version
     and "theirs" is the commit being replayed — this is inverted compared to a
     merge, and getting it backwards silently drops their work.
   - Keep both intents where they're compatible rather than picking a side
     wholesale. If two people edited neighbouring lines, the answer is usually
     both edits, not one.
   - `git add` each file, then:

     ```bash
     bash "$SKILL_DIR/scripts/rebase-sync.sh" continue
     ```

   A rebase replays commits one at a time, so `continue` may hit conflicts again
   and exit 10 again. Loop until it reports `STATUS: rebased`.

   If a conflict is genuinely ambiguous — you can't tell which behaviour the user
   wants — don't guess. Explain the choice and ask. `rebase-sync.sh abort` restores
   the pre-rebase state cleanly if they'd rather handle it themselves.

4. **Confirm before pushing.** Force-pushing rewrites the remote branch, so show
   the user what happened and get an explicit go-ahead first:

   - which commits are now on top of the base (the script prints them)
   - which conflicts you resolved, and how you resolved each one — this is the
     part they most need to sanity-check, since a wrong resolution looks like a
     clean rebase
   - that the next step force-pushes `origin/<branch>`

   Skip this confirmation only if the user already said to push without asking
   (e.g. "rebaseしてforce pushまでやって").

5. **Push**:

   ```bash
   bash "$SKILL_DIR/scripts/rebase-sync.sh" push
   ```

   This uses `--force-with-lease` (plus `--force-if-includes` when git supports
   it) — never a bare `--force`.

   If it reports `push-rejected`, someone else pushed to this branch and the
   script lists the commits that a force-push would destroy. **Stop there.** Do
   not escalate to `--force`, and note that re-running step 2 does *not* help —
   `rebase` only fetches the base branch, so it won't pick up commits made on the
   feature branch itself. Show the user the listed commits and let them decide
   (typically: coordinate with whoever pushed, or `git rebase origin/<branch>` to
   put their own work on top of it).

## Reporting back

Keep the summary short and factual: which path ran (stack / server / plain), the
base branch and its new tip, how many commits were replayed, conflicts resolved
(file + one line on the resolution), and the push result. If anything was left
undone — aborted rebase, rejected push, a path that bailed out — say so plainly
rather than implying success.

Naming the path matters more than it looks: on the server path the user did not
force-push and their commits were re-created by GitHub, so if they later wonder
why the SHAs and committer metadata changed, the answer is in that one word.

