Consider this Git strategy
I wanted to share an alternative Git strategy I have been using for a while now. It’s not the be-all and end-all of Git strategies, but it’s useful in many situations.
It’s somewhat of a hybrid between trunk-based development and GitFlow. I call it rebase-based development.
The end goal is to have a very clear commit history in the main branch that cleanly maps to features, fixes, and other work. This makes it easy to see what has been done and why. If you are using GitOps, and infrastructure definitions are held in the same repository as application code, rolling back or forward between fixes or features becomes much clearer.
Code can also be very easily mapped to tracking systems, such as Jira; this makes it easy to track why a change was made. Useful context for the future. It also makes it harder to slip in unrelated changes.
Git Rebase
Rather than using merge commits (git merge
), which create a lot of extra
clutter in the commit history, git rebase
is used to join a feature branch
into main
.
Before merging, a developer must perform an
interactive rebase
(git rebase -i <COMMIT | HEAD~INT | BRANCH>
) to squash their commits into a
single commit. There’s a small learning curve to interactive rebasing, but every
developer should know how to do it.
On GitHub, you can set git rebase
as the default merge strategy and disable
alternatives.
You lose the ability to merge several commits with a single PR; however, you can simply open multiple PRs.
Additional Protection Rules
The second trick to this strategy is to have additional protection rules for
branches beyond main
.
For example, you might create rules for branches matching feature/*
and
fix/*
.
Instead of merging directly into main
, developers regularly create PRs and
merge into a feature branch. Merging into a feature branch enables regular code
reviews of reasonably sized changes. It also helps developers establish a good
rhythm of submitting PRs on a regular cadence (e.g., daily).
As you’re rebasing into a feature branch, you can do so as often as you like. Smaller PRs have long been known to result in a higher quality review process; and therefore, less bugs, higher quality code, and shorter time to production.
When a feature is complete, it is interactively rebased, subjected to the usual
checks (e.g., unit testing, linting, CI, static analysis), and then rebased into
main
.
A full code review may not be necessary, as smaller PRs into the feature branch will have already undergone reviews. However, in immature codebases with poor testing, you may still want to conduct a full code review.
Contradictions to Trunk-Based Development
The strategy described above somewhat contradicts the principles of trunk-based development because feature branches may become more long-lived.
While I generally agree with the philosophy that feature branches should be short-lived, the reality is that they often are not and cannot be.
Some features may not be easily broken down into small enough pieces. Time is not free and too much planning can lead to not enough doing.
Merging incomplete features into main
can lead to a messy commit history,
messy code, and team confusion about what has actually been completed.
Truly long-lived branches should be avoided at all costs. I have worked in teams that use GitFlow and have seen so much time wasted on merge conflicts. I also found GitFlow caused a lot of confusion, which lead to accidents.
Long-Lived Feature Branches and The Human Factor
There are fewer problems with (somewhat) long-lived feature branches when the team has done adequate planning, teams are appropriately sized, and the codebase is designed to minimize merge conflicts.
Where feasible, split the codebase into smaller files or modules so teams can work on different parts of the codebase without stepping on each other’s toes and causing merge conflicts. There should be a clear separation of concerns and no files that are 1000s of lines long.
During planning, try to assign developers to work on different files or modules.
If teams are too large, merge conflicts are inevitable, and much development time will be wasted resolving them. In such cases, consider splitting the team and codebase into smaller, distinct domains.