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.
75 lines
2.9 KiB
Markdown
75 lines
2.9 KiB
Markdown
# 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.
|
|
|
|
```bash
|
|
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 --orphan` creates 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.
|