# Rebase

> Systematically rebase a branch onto its upstream target. Use when a feature branch has fallen behind, needs conflict resolution, requires commit cleanup before merge, or when a PR review requests a rebase. Handles complex multi-commit branches with many conflicts via incremental strategies.

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

---


# Rebase Branch

Rebase a branch onto its upstream target.

- `$ARGUMENTS` is a **PR number** (all digits) → check out the PR branch and rebase
  onto the PR's base branch.
- `$ARGUMENTS` is a **branch name** → rebase current branch onto that target.
- `$ARGUMENTS` is **empty** → rebase current branch onto the upstream default branch.

## Critical Rules

- **Never rebase without a backup.** Always create a backup ref before starting.
- **Never use `--force`.** Always use `--force-with-lease` when pushing.
- **Never rebase shared branches** without explicit user confirmation.
- **Don't rewrite history unless asked.** The goal is to replay commits onto a new
  base — not to squash, fixup, or reword. Only offer commit cleanup if the user
  explicitly requests it.
- **Verify the result.** Always diff the tree before and after.
- **Conflict sides are swapped from merge.** During rebase: `ours` = upstream,
  `theirs` = your branch. The opposite of what you expect.

## Phase 1: Assess

### Step 1: Determine target and current state

If `$ARGUMENTS` is all digits, treat it as a PR number:

```bash
# Fetch PR metadata
gh pr view "$ARGUMENTS" --json headRefName,baseRefName,headRepository,headRepositoryOwner \
  --jq '{head: .headRefName, base: .baseRefName, repo: .headRepository.name, owner: .headRepositoryOwner.login}'
```

Check out the PR branch locally and capture its tracking remote:

```bash
gh pr checkout "$ARGUMENTS"
CURRENT=$(git rev-parse --abbrev-ref HEAD)
TARGET="<baseRefName from above>"
REMOTE=$(git config "branch.$CURRENT.remote" 2>/dev/null || echo origin)
```

Otherwise, determine target from the argument or defaults:

```bash
TARGET="${ARGUMENTS:-$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null | sed 's|origin/||')}"
DEFAULT_BRANCH="${PRIMARY_BRANCH:-master}"
TARGET="${TARGET:-$DEFAULT_BRANCH}"
CURRENT=$(git rev-parse --abbrev-ref HEAD)
REMOTE="origin"
```

```bash
echo "Rebasing $CURRENT onto $TARGET"
git status --porcelain
```

If there are uncommitted changes, **stop**. Ask the user: stash, commit, or abort.

### Step 2: Measure complexity

```bash
git fetch "$REMOTE" "$TARGET"

# Commits to rebase
git rev-list --count "$REMOTE/$TARGET..HEAD"

# Files changed on our branch
git diff --stat "$REMOTE/$TARGET...HEAD" | tail -1

# How far behind upstream
git rev-list --count "HEAD..$REMOTE/$TARGET"

# Check for merge commits (affects strategy)
git log --merges --oneline "$REMOTE/$TARGET..HEAD"

# Count files changed on BOTH sides (conflict indicators)
comm -12 \
  <(git diff --name-only "$(git merge-base HEAD $REMOTE/$TARGET)..HEAD" | sort) \
  <(git diff --name-only "$(git merge-base HEAD $REMOTE/$TARGET)..$REMOTE/$TARGET" | sort)
```

If **merge commits** exist on the branch, warn the user: a plain rebase linearizes
them. Use `--rebase-merges` to preserve topology, or confirm linearizing is OK.

Report complexity class:

| Commits | Upstream divergence | Class | Strategy |
|---------|-------------------|-------|----------|
| 1–5 | <50 | Simple | Direct rebase |
| 6–20 | <200 | Moderate | Direct rebase |
| >20 or many overlapping files | any | Complex | Incremental rebase |

**Override to Complex** if >5 files changed on both sides, even with few commits.

Only use interactive rebase (`-i`) if the user explicitly asks for commit cleanup.

## Phase 2: Prepare

### Step 3: Safety net

```bash
git branch -f "backup/$CURRENT" HEAD

# Record pre-rebase diff for verification (three-dot: changes since merge base)
git diff "$REMOTE/$TARGET...HEAD" > /tmp/pre-rebase.diff
```

### Step 4: Enable rerere

```bash
git config rerere.enabled true
```

Rerere records conflict resolutions and auto-applies them if the same conflicts
recur. Essential for repeated rebases or incremental strategies.

## Phase 3: Execute

Choose strategy based on complexity class. Default is a straight replay — no
history rewriting.

### Direct rebase (simple / moderate)

```bash
git rebase "$REMOTE/$TARGET"
# If merge commits exist and user wants to preserve them:
# git rebase --rebase-merges "$REMOTE/$TARGET"
```

If conflicts occur, go to Phase 4.

### Interactive rebase (only if user requests commit cleanup)

Only use this when the user explicitly asks to squash, fixup, reword, or
reorder commits. Preview the commit list first:

```bash
git log --oneline --reverse "$REMOTE/$TARGET..HEAD"
```

Ask the user which commits to change. Apply via `GIT_SEQUENCE_EDITOR` to
avoid requiring an interactive terminal:

```bash
# Example: squash commits 2-3 into 1, fixup commit 5
GIT_SEQUENCE_EDITOR="sed -i -e '2s/^pick/squash/' -e '3s/^pick/squash/' -e '5s/^pick/fixup/'" \
  git rebase -i "$REMOTE/$TARGET"
```

Adapt the sed expression to match the user's choices.

### Incremental rebase (complex)

When the branch is far behind or conflicts are expected across many commits,
rebase in stages to keep conflict sets small and let rerere learn resolutions:

```bash
# Find the merge base (where we branched off)
MERGE_BASE=$(git merge-base HEAD "$REMOTE/$TARGET")

# Count upstream commits to choose milestone spacing
BEHIND=$(git rev-list --count HEAD.."$REMOTE/$TARGET")

# Pick 2-3 milestones evenly spaced between merge base and tip
# For 90 commits behind: pick ~30 and ~60
git log --oneline --reverse "$MERGE_BASE..$REMOTE/$TARGET" | head -n 30 | tail -1
git log --oneline --reverse "$MERGE_BASE..$REMOTE/$TARGET" | head -n 60 | tail -1
```

Good milestones are commits that DON'T touch the same files as your branch.

```bash
# Rebase to oldest milestone first
git rebase <milestone-1>
# Resolve conflicts — rerere records them

# Advance to next milestone
git rebase <milestone-2>
# Rerere auto-resolves previously seen conflicts

# Final rebase to tip
git rebase "$REMOTE/$TARGET"
```

**Alternative for very complex cases — cherry-pick one at a time:**

```bash
COMMITS=$(git rev-list --reverse "$REMOTE/$TARGET..HEAD")
git checkout -b "rebase-wip/$CURRENT" "$REMOTE/$TARGET"

for SHA in $COMMITS; do
    git cherry-pick "$SHA" || break
done
```

After all commits applied, point the original branch at the result (safe — the
backup ref preserves the original state):

```bash
git checkout "$CURRENT"
git reset --hard "rebase-wip/$CURRENT"
git branch -D "rebase-wip/$CURRENT"
```

## Phase 4: Resolve Conflicts

When a conflict occurs:

### Step 1: Identify

```bash
git diff --name-only --diff-filter=U
```

### Step 2: Resolve each file

1. Read the conflicted file
2. Understand both sides:
   - **Ours** (upstream): what changed on the target branch
   - **Theirs** (our commit): what we intended to change
3. Resolve by preserving our intent on top of upstream changes
4. For files where one side clearly wins:
   ```bash
   git checkout --theirs <file>   # keep our version
   git checkout --ours <file>     # keep upstream version
   ```
5. Stage:
   ```bash
   git add <resolved-file>
   ```

### Step 3: Continue

```bash
git rebase --continue
```

If a conflict is ambiguous, ask the user before resolving.

### Abort if needed

```bash
git rebase --abort
# or restore from backup:
git reset --hard "backup/$CURRENT"
```

## Phase 5: Verify

### Step 1: Compare diffs

The rebase replays commits onto a new base — the net diff against the target
should be the same. Verify:

```bash
git diff "$REMOTE/$TARGET...HEAD" > /tmp/post-rebase.diff
diff /tmp/pre-rebase.diff /tmp/post-rebase.diff
```

If the diffs differ, investigate. Legitimate reasons:
- Conflict resolutions that accepted upstream changes over ours
- Code refactored on both sides

If unexpected differences exist, **stop** and show the user.

### Step 2: Check for lost commits

```bash
# Commits in backup not in current branch
git log --oneline HEAD.."backup/$CURRENT"
```

Should be empty unless commits were intentionally squashed or dropped.

### Step 3: Run tests if configured

```bash
make test 2>/dev/null || pytest 2>/dev/null || npm test 2>/dev/null
```

If tests fail, **stop** and report the failures to the user before pushing.

## Phase 6: Push

### Step 1: Confirm

Summarize for the user:
- Commits rebased (count)
- Conflicts resolved (count, files)
- Verification result
- Target branch

Ask the user to confirm before pushing.

### Step 2: Force-push with lease

```bash
git push --force-with-lease "$REMOTE" "$CURRENT"
```

If rejected (remote ref moved since last fetch), run `git fetch "$REMOTE" "$CURRENT"`
and assess whether someone else pushed to the branch. Ask the user before retrying.

### Cleanup

```bash
git branch -D "backup/$CURRENT"
rm -f /tmp/pre-rebase.diff /tmp/post-rebase.diff
```

## Recovery

If something went wrong after the rebase:

```bash
# Option 1: Backup branch
git reset --hard "backup/$CURRENT"

# Option 2: Reflog (backup already deleted)
git reflog
git reset --hard HEAD@{N}   # N = pre-rebase entry
```

