Six Branches, Five Paths, Eight Rules: The Git Workflow That Ended All-Night Merge Conflicts
Git Commit Standards
Author: 挖掘狂人 (Digging Maniac)
Clear standards, fewer conflicts, efficient collaboration.
A few years ago, I took over a six-person team. The code was decent, but the collaboration style was 'wild': everyone developed on a single branch, and the week before release was spent resolving merge conflicts — all-night sessions.
Worse, one time a bug appeared in production. The on-call engineer directly changed a few lines on main, verified it, and shipped. Two weeks later, when the new version went live, the same bug resurrected. After a long investigation, we found that the fix had never been merged back to the development branch.
After that post-mortem, I rewrote the team's entire Git collaboration process. That process followed me through two teams and ran stably for two or three years. Today, I've organized the complete practice here for your reference and trimming.
I. Why I No Longer Trust 'Verbal Agreements'
The early team wasn't without standards; they were written in the notes of a departing colleague. Actual execution relied on 'asking Lao Wang.' Eventually, four major problems emerged:
| Symptom | What I Saw | Root Cause |
|---|---|---|
| Conflicts explosion | Entire team stayed up all night resolving conflicts the week before release | Everyone shared one branch for development |
| Environment mismatch | PMs pointed at DEV pages and asked when it would launch | Branches and deployment environments had no one-to-one mapping |
| Unreadable history | git log full of update, modify, fix bug |
No unified commit message convention |
| Firefighting chaos | Hotfix only changed main, bug reappeared in the next version |
Online fixes never merged back to the development trunk |
These four pieces interlock: the branch model determines where code is changed, environment mapping determines version correspondence, commit standards determine issue traceability, and process discipline backs it all up. Missing any one of them cancels the benefits of every other standard.
II. The Six Branches We Settled On
We first tried the minimal version (main + feature); with seven or eight people, it collapsed. We also tried the full git-flow, which small teams found too heavy.
The final compromise: six branches, each with a clear role and strictly separated lifecycle.
| Branch | Role | Source Branch | Merge Destination | Lifecycle |
|---|---|---|---|---|
main |
Production trunk, always equal to the live version | release, hotfix |
In-only, out-forbidden; no merges out | Long-lived |
develop |
Development trunk, always kept runnable | feature, hotfix |
Merge to test |
Long-lived |
feature/* |
Single requirement/module development branch | develop |
Merge back to develop |
Deleted immediately after the requirement ships |
test |
Testing branch for QA verification | develop |
Merge to release |
Exists during a version iteration |
release/* |
Pre-release UAT acceptance branch; only bug fixes, no new features | test / hotfix |
First merge to main, then back into develop |
Per-version cycle |
hotfix/* |
Emergency production fix branch | main |
Merge to both main and develop |
Created on demand |
Memory mnemonic for new hires:
mainis the fact that is already live,developis the plan about to be released,featureis an idea still being developed.
Mix these up, and most release incidents will follow.
Tip: Name
featurebranches by module, e.g.,feature/user_module; avoid meaningless names likefeature/dev1orfeature/aaa, so nobody has to guess the branch's purpose later.
III. Only Five Flow Paths — Memorize Them and You're Ready
The core of the branch model isn't branch names, but fixed flow paths. New hires only need to master the five processes below, and most collaboration problems resolve themselves.
Path 1: Daily Requirement Development (develop → feature → develop)
# Create a feature branch from the development trunk
git checkout develop
git pull
git checkout -b feature/xxx_module
# After development, merge back to develop; must use --no-ff to preserve branch history
git checkout develop
git merge --no-ff feature/xxx_module
Path 2: Version QA Submission (develop → test)
After self-testing and MR review pass, merge code to the test branch for QA.
git checkout test
git merge --no-ff develop
Path 3: Version Release (test → release → main → develop)
# Create pre-release branch from test
git checkout test
git checkout -b release/v1.4.0
# After acceptance, merge to production main
git checkout main
git merge --no-ff release/v1.4.0
# Key: merge all bug fixes back to the development trunk
git checkout develop
git merge --no-ff release/v1.4.0
Path 4: Emergency Production Fix (main → hotfix → main + develop)
# Create fix branch from production main
git checkout main
git checkout -b hotfix/fix_online_bug
# After fixing, first merge to production
git checkout main
git merge --no-ff hotfix/fix_online_bug
# Critical: must also merge to develop, otherwise the bug resurrects in the next version
git checkout develop
git merge --no-ff hotfix/fix_online_bug
The production incident at the start was exactly this flow being only half-completed. Merging only to main and not back to develop is deliberately planting a mine for the next version.
Path 5: Production Rollback (No New Branches)
For serious production issues, prefer git revert to create a reverse commit. This keeps history complete and traceable — who did the rollback is obvious at a glance.
Forcing a reset --hard on a main that has already been pushed is strictly forbidden: rewriting the shared baseline saves ten minutes of debugging but costs an entire night fixing everyone's code baseline.
IV. One Branch, One Environment Only
The branch model is the skeleton; the one-to-one branch-environment mapping is the blood vessel. The biggest pitfall we hit was never a Git operation problem, but a version-environment mismatch: PMs brought DEV half-finished features as change requests, QA accepted incomplete code in a mixed environment, and nobody could tell which version was actually live.
So we fixed the mapping and forbade anyone from breaking it:
| Branch | Deployment Environment | Public Access | Constraint Rule |
|---|---|---|---|
main |
PRO production | Open | All commits must pass code review + CI checks; direct pushes forbidden |
develop |
DEV development | Open | For developer self-testing, unstable code allowed |
feature/* |
No environment | Forbidden | No entry until the feature is done, preventing half-finished work from being accepted as a real version |
release/* |
FAT acceptance | Open | PM and QA sign off here |
hotfix/* |
Temporary UAT | Internal temporary verification only | Only allowed to merge to production main after verification passes |
Abbreviation notes to avoid confusion:
- PRO: Production
- DEV: Development
- FAT: Functional Acceptance Testing
- UAT: User Acceptance Testing
A common new hire question: why can't feature branches be deployed to a test environment? The moment you open public access, someone will treat the half-finished product as a final version, and testing conclusions become invalid. Restricting access isn't being difficult; it's cutting off version misuse at the source.
Environment mismatch makes problem diagnosis almost mystical — the code version and page behavior lose their one-to-one relationship, and you waste huge amounts of time verifying which version you're looking at.
V. Commit Standards: Turning Logs Into a Searchable Asset
Early on, we only had the soft rule of 'write clear commit messages,' and the log still filled with update and modify code.
Later we enforced a unified set of 7 commit types, which completely cured the log chaos.
Unified format: <type>(<scope>): <subject>
- scope: corresponding module name
- subject: imperative sentence, no trailing period, within 50 characters
| Type | Meaning | Standard Example |
|---|---|---|
feat |
New business feature | feat(order): support filtering orders by time range |
fix |
Fix a production/test bug | fix(cart): fix still allowing order when quantity is 0 |
docs |
Documentation-only changes | docs(readme): add local startup steps |
style |
Code format changes, no logic | style: unify indentation to 2 spaces |
refactor |
Refactor, no new features or bug fixes | refactor(user): extract phone number validation utility |
perf |
Performance optimization | perf(list): add index to list endpoint, reduce latency by 60% |
test |
Add/modify unit test cases | test(pay): add refund failure scenario |
Good vs. Bad Examples
| Bad Commit | Standard Good Example |
|---|---|
update |
feat(user): add passwordless login |
modify bug |
fix(order): fix negative amount when coupons stack |
commit something |
refactor(api): unify error code return structure |
The benefits of following this convention go far beyond clean logs:
- Automatic changelog generation;
- Code review can be prioritized by commit type:
fixfocuses on edge cases,refactorensures behavior is unchanged; - Semantic version numbers can auto-upgrade:
featbumps minor,fixbumps only patch.
New hires easily confuse refactor and style: only indentation, whitespace, or line breaks belong to style; restructuring code while leaving business behavior exactly the same is refactor. Both types strictly forbid carrying business logic changes. Hiding a functional change inside a style commit easily bypasses code review, a very high risk.
VI. Eight Iron Rules I Set (All Enforced by Tools)
Rules written in a wiki rarely stick; you must encode them into processes and tooling. We enabled mandatory enforcement for all eight rules below:
mainandreleasehave branch protection; nobody can push directly, including admins.- All code merged into
mainmust go through MR, with at least one reviewer and all CI pipelines passing. featurebranches live no longer than 3 days; large requirements must be split into smaller iterations.- All merges use
--no-ffto preserve complete branch topology, sogit log --graphstays traceable. - Every formal release gets an annotated tag (format:
v1.4.0), giving rollbacks a clear anchor. - Hotfixes must perform a double merge: into both
mainanddevelop; neither is optional. - Run
pull --rebasebefore every commit/push to reduce meaningless merge commits. releasepre-release branches only accept bug fixes; any new feature goes back todevelopfor the next version.
VII. Tuition I've Paid Over the Years
Every rule corresponds to a production incident. A few most memorable:
- Hotfix only merged to main, forgot to merge to develop — the opening incident. We then wrote double-merge directly into the process template; an MR can't be submitted unless the develop merge option is checked.
- A feature branch sat dormant for a month — one requirement took three weeks to raise an MR, with 500+ files causing massive conflicts; two people manually merged line by line overnight. Now I proactively follow up to split requirements that haven't merged within 3 days.
- A new feature was added to a release branch — the version had already passed UAT, but a developer casually added a button, forcing a full regression test. Now, any non-fix commit on a release branch causes CI to fail and block the merge.
- Using fast-forward merges throughout — history looked like a straight line, but you couldn't see which branch started and ended each feature; when production failed, there was no way to roll back by feature.
- Pushing without pulling the latest code — now it's team muscle memory: the first thing every morning is pulling the latest remote code to avoid large-scale conflicts.
One more high-frequency pitfall: branch and environment mismatch. Deploying release branch code to the DEV environment, or vice versa, leads to endless internal friction, constantly re-verifying 'are we looking at the same version?'
VIII. If You Can Only Do One Thing First
Standards carry explicit costs: writing an extra commit message, filing an extra MR, waiting an extra CI round. But the implicit benefits are enormous: no all-night conflict marathons, no reconstructing incident scenes from memory, no endless code-change archaeology.
Don't try to implement the full six-branch model at once — the pushback is too strong, and you'll likely give up halfway.
Roll out in two steps:
- Enable
mainbranch protection; forbid direct pushes. - Force all commits to carry a type prefix like feat/fix/docs.
Both changes are minimal, barely alter your existing process, and within two weeks your messy git log becomes clear. Once the team tastes the sweetness of easy traceability, you can gradually roll out the rest of the branch management discipline.
Final summary: a disciplined Git collaboration process is never about constraining developers; it's about giving the team's energy back to actual development instead of code management.