The GitHub's “Impossible Triangle” of Project Collaboration
The “Impossible Triangle” of Project Collaboration on GitHub
Let’s introduce the three (heavily opinionated) ideals of a Git project, and explain why achieving all three simultaneously is currently impossible using GitHub’s native tools alone.
Hand-crafted Linear History on Main
The main branch consists of a single, straight line of commits:
- Each commit is a logical, atomic change crafted by its original author.
- The result is history you can read, search, and bisect easily, with true cause-and-effect visible.
- No unnecessary noise from merge or squash commits.
This requires discipline and some effort. If commits are not meaningful and well-structured, then hand-crafted linear history does not bring any value.
PR-based Review and CI for All Changes
No change lands without automated verifications. You won’t accidentally push code that doesn’t build, fails tests, or lacks required reviews. Every change goes through a Pull Request with CI checks before merging.
All Commits Are Cryptographically Signed and Author-verifiable
Every commit on the main branch is locally signed by the developer who created it, using their GPG key. You can always prove who authored a given change.
Why the Trio is Impossible on GitHub (with native tools alone)
Using GitHub’s official Pull Request workflow, branch protection rules, and GitHub Actions integration, you cannot achieve all three ideals at once. The platform’s available merge strategies force you to compromise.
You can try picking two of the three ideals, but the third will always be sacrificed:
Signed Commits + Hand-crafted Linear History (No PR-based CI)
Contributors rebase and push directly to the main branch. All original, signed commits are preserved in a linear, logical order. But: you lose automated staging, testing, and required code review for every change—so quality and safety depend on discipline outside GitHub’s guardrails.
Hand-crafted Linear History + PR-based CI (No Author-signed Commits)
If you choose the Rebase merge method, the PR commit history is kept and the main branch stays strictly linear, but every commit is re-created at merge time (with a fresh committer’s timestamp), and the original author signatures are lost. The main branch receives unsigned, system-generated commits which map 1:1 to the original ones.
Signed Commits + PR-based CI (No Hand-crafted Linear History)
If you pick the Merge merge method, the original author-signed commits are preserved, but merge commits land on the main branch, resulting in a non-linear history.
The Last Merge Method: Squash Merges
Using the Squash merge method breaks all three ideals as I described them:
- The history is linear, but the commits are potentially heavy and complex.
- Bisecting is more difficult because multiple logical changes are combined into one commit.
- The new commit is created by the person merging the PR and signed by GitHub, not the original author or the person merging the PR
At the same time, squash merges can be a reasonable compromise for many teams. This is especially true for Open Source projects accepting contributions from volunteers, where enforcing strict commit hygiene is impractical.
You can mitigate some downsides of squash merges by promoting small PRs with a single logical change.
My Compromise
I decided to go with Signed Commits + PR-based CI + Merge Commits, accepting the non-linear history as a trade-off for safety and verifiability. Additionally, if you enable “Require branches to be up to date before merging” branch protection option, you ensure that all commits are tested against the latest main branch state before landing. The history is not linear, but it’s reasonably close.
Possible Solution: Fast-Forward Merge
If GitHub offered fast-forward-only PR merging, you could:
- Require all PRs to rebase onto latest main before landing
- Merge without squashing or generating new commits, preserving both commit signatures and history
- Enforce CI and review before allowing merge
This would satisfy all three ideals. As of today, this is not possible with built-in GitHub tools.
There’s an open GitHub feature request. It is four years old at the time of writing and has no official response, so don’t hold your breath.

Comments
Post a Comment