Claude
Skills
Sign in
Back

git-rebase

Included with Lifetime
$97 forever

Use when squashing fixup commits into earlier commits, cleaning up a feature branch with interactive rebase, recovering a dropped or failed autosquash, inserting a reformatting commit before code changes, moving file changes between commits, splitting one working-tree edit across several historical commits, or diagnosing autosquash conflicts.

General

What this skill does


# git-rebase

## Overview

The standard fixup workflow is: create a `fixup!` commit in the branch, then `GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>`. Manually editing the todo file or writing a custom sequence editor script is almost never necessary and introduces failure modes.

**Core principle — reconstruct, don't merge.** Every pattern below for rearranging commit contents rests on one idea: rather than replaying patches through three-way merge (which conflicts the moment surrounding context has shifted), take each file's *complete, correct state* — or a clean per-target slice of it — and commit that directly. No merge means no conflicts. When a procedure says "why this works," this is why.

## Safety

**Before any rebase**, note current HEAD:
```bash
git log --oneline -1  # copy this SHA
```

**Recovery after a bad rebase:**
```bash
git reflog              # find the pre-rebase HEAD@{N}
git reset --hard HEAD@{N}
```

**Never rebase while parallel agents have staged changes.** Staged changes are shared working-tree state. If another agent commits while you're mid-rebase, the commits become entangled. Finish or abort all rebase operations before handing off to parallel agents.

## Before You Start: Audit Per-File Targets

**One fixup commit can squash into exactly one target commit.** If your working-tree change to a file needs to land in multiple historical commits, you need multiple fixup commits — each containing only the slice that belongs in its target.

This is the single most common cause of mid-rebase conflicts. Catch it upfront:

```bash
# For each file you've modified, see which branch commits already touched it
git status --short | awk '{print $2}' | while read f; do
  echo "=== $f ==="
  git log <base>..HEAD --oneline -- "$f"
done
```

If a file appears in only one commit: a single fixup is fine.
If a file appears in N commits: you'll need to split the diff across N fixups. See [Splitting One Working-Tree Change Across Multiple Fixup Targets](#splitting-one-working-tree-change-across-multiple-fixup-targets) below.

## Core Workflow

**1. Create the fixup commit**

```bash
# Stage your changes, then:
git commit -m "fixup! <exact subject of target commit>"
```

The message after `fixup! ` must match the target commit's subject verbatim. `git commit --fixup <sha>` generates this automatically.

**2. Verify the fixup is in the rebased range**

```bash
git log <base>..HEAD --oneline | grep "fixup!"
```

If it's not listed, the autosquash will silently have nothing to squash. See "Dropped fixup recovery" below.

**3. Autosquash**

```bash
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
```

`GIT_SEQUENCE_EDITOR=true` accepts the autosquash-generated todo without opening an editor. Git arranges the `fixup` line correctly on its own. No custom script needed.

**4. Verify the result**

```bash
git log --oneline -10          # find the new SHA (rebasing rewrites SHAs)
git show <new-sha> --stat      # confirm expected files are in the right commit
```

"Rebase succeeded" ≠ "rebase did what I intended." Always inspect the commit.

**On long branches with many fixups, prefer incremental autosquash.** Commit one fixup, autosquash, verify, then create the next. Batching seven fixups and running one autosquash means conflicts surface in arbitrary mid-rebase order with no cheap way to course-correct — if fixup #1 turns out to span two commits, you discover it three commits into the rebase instead of before starting.

## Dropped Fixup Recovery

If a fixup commit was dropped from the branch by a previous rebase:

**Don't** try to manually insert the old SHA into the todo file. Instead, re-create the commit from scratch:

```bash
# Find the dropped commit
git reflog | grep "fixup!"

# Inspect it
git show <dropped-sha>

# Re-apply its changes to the working tree
git checkout <dropped-sha> -- <file>    # for file changes
# or apply the diff manually

# Create a new fixup commit
git add <file>
git commit -m "fixup! <target subject>"

# Now autosquash normally
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
```

## Custom GIT_SEQUENCE_EDITOR Scripts

**Avoid them.** `--autosquash` handles standard `fixup!` cases without any script. Custom scripts are only needed when you want non-standard rearrangements.

If you must write one, it **must be two-pass**:

```python
#!/usr/bin/env python3
import sys, re

todo_path = sys.argv[1]
with open(todo_path) as f:
    lines = f.readlines()

# PASS 1: build map of fixup targets → fixup lines, collect non-fixup lines
non_fixup = []
fixups = {}  # target_subject → [fixup_line, ...]
for line in lines:
    m = re.match(r'^pick (\S+) fixup! (.+)\n', line)
    if m:
        target = m.group(2)
        fixups.setdefault(target, []).append('fixup ' + m.group(1) + ' fixup! ' + target + '\n')
    else:
        non_fixup.append(line)

# PASS 2: insert fixup lines after their targets
result = []
for line in non_fixup:
    result.append(line)
    stripped = line.strip()
    if stripped and not stripped.startswith('#'):
        subject = stripped.split(None, 2)[2] if len(stripped.split(None, 2)) == 3 else ''
        for fixup_line in fixups.pop(subject, []):
            result.append(fixup_line)

# Append any unmatched fixups rather than silently dropping them
for lines_list in fixups.values():
    result.extend(lines_list)

with open(todo_path, 'w') as f:
    f.writelines(result)
```

**The single-pass trap:** Processing the todo top-to-bottom fails when the target commit appears *before* the `fixup!` line (i.e., always — the target is older). A single-pass script will check `if fixup:` for the target line when no fixup has been seen yet, store the fixup, and never emit it.

## Choosing a Reconstruction Pattern

The next three sections rearrange commit contents. They all apply the "reconstruct, don't merge" principle above; pick by what you start from:

| You have… | …and want to | Pattern |
|-----------|--------------|---------|
| A branch mixing formatting and logic changes | A pure reformatting commit first, then code-only commits | **Replay-and-reformat** |
| Existing commits whose files belong in *different* commits | Recombine file states across those commits | **Checkout-and-reconstruct** (Moving File Changes) |
| *One* uncommitted edit | Slice it across *multiple* historical commits | **Per-slice fixups** (Splitting) |
| One edit that is a mechanical, idempotent transform (rename, formatter) | Auto-slice it across the commits it touches | **`git rebase --exec`** shortcut |

## Inserting a Reformatting Commit Before Code Changes

**Replay-and-reformat.** When a branch mixes formatting changes (e.g., from `ruff format`, `black`, `prettier`) with logic changes, split them into a pure reformatting commit followed by code-only commits. Do **not** cherry-pick or rebase the code commits onto a reformatted base — every hunk conflicts because the surrounding context changed (quotes, line wrapping, indentation), producing dozens of unresolvable conflicts. Reconstruct instead:

```bash
# 1. Note current HEAD for safety
git log --oneline -1

# 2. Create a branch at the commit just before code changes
git checkout -b temp-branch <last-pre-code-commit>

# 3. Create the reformatting commit
<formatter> <files>
git add <files>
git commit -m "Reformat with <tool>"

# 4. Replay each code-change commit by checking out its file state
#    from the original branch, reformatting, and committing
for sha in <code-commit-1> <code-commit-2> ...; do
    git checkout "$sha" -- <files>
    <formatter> <files>
    git add <files>
    msg=$(git log -1 --format="%B" "$sha")
    git diff --cached --quiet || git commit -m "$msg"
done

# 5. Verify final content matches original (reformatted)
git show <original-HEAD>:<file> > /tmp/orig
<formatter> /tmp/orig
diff /tmp/orig <file>  # should be empty

# 6. Update the original branch
git branch -f <original-branch> HEAD
git checkout <original-branch>
git branch -D temp-branch
`

Related in General