layered-soul/skills/forgejo-operations/references/git-history-flatten.md
Sam & Claude 4d8ce07fa7 docs: apply Prettier to current markdown (Sam & Codex)
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.
2026-06-14 01:48:32 +02:00

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.