Normalize markdown formatting after the latest main updates.\n\nChecks: python3 scripts/layered_soul.py validate .; npx --yes prettier@3 --check '**/*.md'; git diff --check.
2.9 KiB
2.9 KiB
Git History Flattening — First-Parent Cherry-Pick
Flatten a merge-heavy git history into a linear sequence of first-parent
commits — merge nodes become regular commits, no information lost. Produces
a history where git log --first-parent IS the full log.
Used when: repo has accumulated many merge nodes (e.g. PR-per-feature
workflow with --no-ff merges) and you want a clean linear history.
When to use
- Merge noise is high (e.g. 38 merge nodes in 430 commits)
- History should be linear but content must be identical
- Repo pack is small enough that flattening is cosmetic, not space-driven
Technique: orphan branch + first-parent cherry-pick
This is safer than git rebase -i because it doesn't touch the original
branch until the new history is fully built and verified.
cd /path/to/repo
# 1. Backup current main
git branch main-pre-flatten HEAD
# 2. Create orphan branch (empty, no history)
git checkout --orphan flat-main
git rm -rf --quiet . 2>/dev/null || true
# 3. Cherry-pick all first-parent commits in chronological order.
# Merge commits get -m 1 (take main-line side).
# Regular commits cherry-pick normally.
git log --reverse --format='%H' --first-parent main-pre-flatten | while read hash; do
parents=$(git rev-list --parents -n 1 "$hash" | wc -w)
# parents count includes the commit itself: 2=regular, 3+=merge
if [ "$parents" -ge 3 ]; then
git cherry-pick -m 1 "$hash" --allow-empty
else
git cherry-pick "$hash" --allow-empty
fi
done
# 4. Verify tree is identical
git diff main-pre-flatten flat-main --stat # should be empty
# 5. Swap branches
git branch -D main
git branch -m flat-main main
Result
| Before | After | |
|---|---|---|
| 430 commits, 38 merge nodes | 194 linear commits, 0 merges | |
| `git log --oneline --merges | wc -l` = 38 | = 0 |
git diff old-main new-main |
empty |
Pitfalls
- Remote has branch protection: must temporarily remove protection,
force push, re-create. See
references/branch-protection-api.md. - Other remotes: push to all remotes (
forgejo+origin) to keep tracking refs in sync. - Worktrees on the FreeBSD build host: they may be detached at the
old HEAD. After force push they can
git fetch && git reset --hard origin/main— the new HEAD is the same tree, just different commit SHA. - Orphan branch must be truly empty:
git checkout --orphancreates a branch with no commits, but the index still has staged files.git rm -rf --quiet .clears them before cherry-picking. - First-parent view is the target: 38 original merge commits vs 31 on first-parent — the 7 missing are "merge main back into feature branch" type merges that don't appear on the first-parent line and are correctly excluded.