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

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 --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.