跪拜 Guibai
← Back to the summary

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:

main is the fact that is already live, develop is the plan about to be released, feature is an idea still being developed.

Mix these up, and most release incidents will follow.

Tip: Name feature branches by module, e.g., feature/user_module; avoid meaningless names like feature/dev1 or feature/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:

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>

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:

  1. Automatic changelog generation;
  2. Code review can be prioritized by commit type: fix focuses on edge cases, refactor ensures behavior is unchanged;
  3. Semantic version numbers can auto-upgrade: feat bumps minor, fix bumps 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:

  1. main and release have branch protection; nobody can push directly, including admins.
  2. All code merged into main must go through MR, with at least one reviewer and all CI pipelines passing.
  3. feature branches live no longer than 3 days; large requirements must be split into smaller iterations.
  4. All merges use --no-ff to preserve complete branch topology, so git log --graph stays traceable.
  5. Every formal release gets an annotated tag (format: v1.4.0), giving rollbacks a clear anchor.
  6. Hotfixes must perform a double merge: into both main and develop; neither is optional.
  7. Run pull --rebase before every commit/push to reduce meaningless merge commits.
  8. release pre-release branches only accept bug fixes; any new feature goes back to develop for the next version.

VII. Tuition I've Paid Over the Years

Every rule corresponds to a production incident. A few most memorable:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

  1. Enable main branch protection; forbid direct pushes.
  2. 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.