There Is Only One Question That Picks a Strategy
One-line summary
The question to ask when choosing a branching strategy is not "which diagram looks nicest" but "when an outage happens, how do we plan to find the culprit commit in this repository?"
Why this is needed
Git Flow, GitHub Flow and Trunk-Based were each designed on the assumption of a different release cadence. If you ignore the assumption and take only the name, you get friction every day.
| Strategy | Branch kinds | Merge frequency | Where it fits |
|---|---|---|---|
| Git Flow | 5 or more kinds | 1–2 per week | Scheduled releases, mobile apps, packages |
| GitHub Flow | 2 kinds | 1–3 per day | SaaS that practices CD |
| Trunk-Based | 1–2 kinds | 5–10 or more per day | Branch lifetime within 1 day, Feature Flags required |
Trunk-Based demands a very high level of CI. If you start without Feature Flags, unfinished code is deployed as it is. Conversely, in a GitOps environment, Git Flow's release branches clash heavily with the automatic sync pattern. So if you use GitOps, GitHub Flow or Trunk-Based is recommended.
One common rule works anywhere. A feature branch that lives for 3 days or more is a breeding ground for merge conflicts.
How it works
There are three merge methods, and each leaves something different behind.
--no-ffmerge: a commit with two parents is created, so "when this bundle was integrated" remains in the history.- Fast-forward: no merge commit; only the branch pointer moves forward. The history becomes a straight line, but the integration point is not kept.
- Squash merge: presses the commits into one. It looks tidy but takes three things away. The resolution of binary search (bisection) becomes fixed at the PR size, cherry-pick precision is lost, and from Git's point of view that branch has never been merged (because the new commit's parent is not the original branch). So a team that uses squash merges must also adopt the rule of deleting the source branch right after the merge.
A cherry-pick is not cutting and pasting a stored diff. Git does not store diffs in the first place; it stores only snapshots. So it performs a three-way merge using the parent tree of the target commit as the common ancestor. This is why a cherry-pick can conflict.
What it looks like in the field
A hotfix is made on the release branch and not applied to main, so the same bug comes back to life in the next release — every team experiences this once. If you run release branches, "a hotfix must always be brought into main too" must be written as a policy sentence.
The same goes for tags. A lightweight tag is just a pointer, so nothing remains about who attached it, when or why. A release tag should be annotated with -a so that it can be used in an investigation later.
Keep the history for investigation
The value of a branching strategy shows not in ordinary times but when an incident occurs. In a situation like "it worked until yesterday but not today", the standard tool for finding the culprit commit is binary search. Git does this semi-automatically with git bisect. If you tell it a good commit and a broken commit, it checks out the midpoint, and if a person only answers good/bad, it halves the range each time. Ten steps are enough to find the cause among 1,000 commits.
For binary search to work well, the history must satisfy two conditions. Every commit must build and work, and commits must be small. If commits that do not work are mixed in, a person cannot answer good/bad and has to skip that point, and if a squash merge has turned each PR into a single commit, the search stops in front of that PR. That means you find out only "somewhere in this PR" and have to read the rest by hand. This is the passage behind the earlier statement that squash merges fix the bisection resolution at the PR size.
The choice after finding the culprit commit should also be decided in advance. There are two ways to undo it, and their natures are completely different.
git revertcreates a new commit that cancels that change. The history is preserved, and it is safe even on an already shared branch. On a production branch, use only this.git resetmoves the branch pointer backward. If you do it on a shared branch, the history diverges from other people's repositories, and everyone afterward suffers from force pushes and conflicts.
When reverting a merge commit, one more thing is needed. Since it has two parents, you must specify which line to keep, as in -m 1, and usually parent 1 is the side that received the merge (main). And if you merge the same branch again after reverting a merge, no changes come in. From Git's point of view they are commits that have already been merged. It is better not to learn for the first time, in an urgent situation, that to bring the reverted changes back you have to revert the revert commit once more.
What you will do in the next lab
You will create a repository from scratch, merge a feature branch with --no-ff to leave an integration record, raise small fixes by fast-forward, attach a release tag, bring the hotfix from the release branch into main with a cherry-pick, and then clean up only the merged branches. At the end, you count the actual numbers in this repository and write a strategy comparison table.