Git Flow branching: where release breakages begin
Git Flow rests on the agreement that each branch has its own role: permanent branches for development and released state, temporary ones for tasks, stabilization, and urgent fixes.
As long as everyone follows the rule, the branch scheme can be read at a glance, and commit history answers what exactly went into a release. The first deviation from it breaks both of these guarantees.
A typical breakage starts with a task being merged into the wrong branch or edited directly in the stable branch. Then come merges from other people's branches and cherry-picks of individual commits to "patch up" the release to the desired state.
Commit history turns into a tangle where you can no longer reconstruct which changes belong to the release and which got into it by accident.
This is how an unstable release is born. The task branch has no explicit readiness marker, and during the build everything that was merged by the freeze point goes into it — along with unfinished work and temporarily disabled checks.
A regression surfaces in prod, and finding the cause starts with manually sorting through branch divergences instead of reading history.
A separate story is the urgent fix. According to the branching strategy, it starts from the released state and returns to two long-lived branches at once.
If it is done directly in the release branch, the fix is lost during the next merge, and the same defect returns one or two releases later.
Below we break down how this is closed: which rules are fixed so that the branch scheme remains verifiable and the release is assembled only from ready changes.
What a managed branching and merging scheme provides
Long branches that live in parallel for weeks are the main source of pain: the longer a branch diverges from the mainline, the more edits intersect in the same files.
Merging stops being a minutes-long operation and turns into resolving merge conflicts, where half the decisions have to be made again. It is precisely at this stage that changes are most often lost.
A managed Git Flow scheme defines short task branches and predictable merge points: merging rules are described in advance, checks and automated tests pass before code reaches the mainline.
Merge time is reduced many times over — divergence is limited and visible in advance, so a typical set of changes reaches the mainline without manual conflict resolution.
Then version control works: history reads as a sequence of releases, not as a set of parallel lines. Every change is tied to a task and to a release, and rollback is tied to a specific point.
Hence release predictability: the release composition is known before the build, rather than discovered after the fact.
Release stability is measured by the share of rejected releases and recurring failure classes.
After implementing the scheme, failures due to unmerged edits and "forgotten" fixes in the release branch go away, while hotfixes follow a separate short path and do not mix with new development.
Branching models: Git Flow and short branches
The choice between the full Git Flow model and short branches is determined by release frequency and the number of supported versions. Full Git Flow maintains permanent development and main branches, separate release branches, and fix branches.
This provides an isolated stabilization environment: the testing environment is built from the release branch while the team continues working in the development branch.
The short-lived branch model is structured differently: a branch lives from a few hours to two days and is immediately merged into the main branch, and the release comes from it as well.
The scheme is simpler and faster, but relies on automated checks — without a quick test run for each merge, it falls apart into manual conflict resolution.
| Model | When it fits | Limitations |
|---|---|---|
| Git Flow with release branches | planned releases, multiple supported versions, separate testing environment | long-lived branches diverge, share of manual merges grows |
| Short branches, release from main | frequent releases, one supported version, mature automated checks | discipline and quick test runs are needed; with weak coverage the risk of failure grows |
| Hybrid: short branches plus a release branch during stabilization | frequent releases with mandatory checks in a separate environment | the release branch lives long and diverges from the main branch again |
The model's limitations show at scale: the more parallel release branches and the less frequent the merges, the higher the cost of conflicts and the later regressions are found.
Therefore we fix the model according to release cadence: planned and infrequent — full Git Flow; frequent with one version — short branches with a release branch only during stabilization.
Implementation order: from repository audit to handover
Implementing Git Flow is not about creating a develop branch but about restructuring how the team ships changes. We start with a repository inventory and only then design the scheme.
- Repository audit — a map of branches, tags, and merges over several releases: dangling branches, conflicting access rules, places where code went directly into the main branch. The output is a list of bottlenecks and an estimate of the scope of work.
- Branch scheme design — based on release cadence, we choose the model: long-lived branches for release preparation, short ones for tasks, separate ones for urgent fixes in prod. We fix naming, merge order, and who applies the tag.
- Regulations — commit message format, requirements for change size, review policy, and the procedure for synchronizing long-lived branches with the main branch so that divergence does not accumulate by release time.
- Protection setup — mandatory checks before merging, prohibition of direct pushes to protected branches, requirement of a successful build and review, release permissions. The rules work at the repository level, not on trust.
- Testing on live tasks — we run the scheme on real but non-critical tasks: release preparation, urgent fix, rollback. We see where the process slows down and fix it before handover.
- Handover to the team — branch documentation, commit templates, instructions for new developers, walkthroughs of the scenarios "how to ship a release" and "how to roll back".
The readiness criterion is that the team ships and rolls back a release without external help, and the repository history reads as a delivery log.
Branch protection rules, schemes, and regulations
Protection rules and role permissions most often remain an oral agreement: while the team has two or three engineers, the scheme holds on discipline.
As soon as there are more people or an external contractor is involved, the first urgent fix goes directly into the main branch — and it is no longer possible to check what exactly went into the release. Therefore we fix protection settings and regulations in the repository, not in chat.
What remains with the customer after setup:
- Branch protection rules: direct writes to the main and release branches are closed; merging is possible only through a request with mandatory review and passed checks.
- Role permission matrix: who merges into a release, who can remove a block in an exceptional situation, who only proposes changes — with separation by teams and contractors.
- Release regulations: from which branch the release branch is cut, which tags are applied, in what order changes return to the main branch.
- Urgent fix procedure: the branch is cut from the tag of the last release, and then must be merged back into the main and current release branches — otherwise the fix is lost in the next release.
- Rollback scheme: rules for applying tags and the procedure for returning to the previous release without manually editing code on the server.
- Branching documentation: a Git Flow scheme describing the lifecycle of each branch, naming rules, and branch lifetime before deletion.
- Merge request templates and readiness criteria: what must be in the description, which checks are considered mandatory.
The regulations live in the repository next to the code — a new engineer reads the rules instead of figuring them out in chat.
Case study: fewer conflicts and urgent fixes in a release
A product team of nine engineers maintained two development lines in one repository: edits went directly into the main branch, while release preparation lived in a separate long-lived branch.
By merge time, divergence reached hundreds of files; conflicts were resolved manually by two engineers and consumed up to two days. A version was released once every two weeks, and roughly every third release was accompanied by an urgent fix already in the running environment.
What the analysis showed
An analysis of the history in GitLab showed that the problem was not the engineers' carelessness. Branches had no naming rules or maximum lifetime, the main branch was not protected — direct pushes went through without review, and automated checks ran only on the release branch.
Because of this, defects surfaced during version preparation, when the cost of a fix is highest, and the cause of the conflict was lost in the stream of edits.
What was changed
We switched to short-lived branches for each task with a lifetime of two to three days and protected the main branch: merging only through a merge request, mandatory review by a second engineer, green checks, linear history without extra merge points.
For urgent fixes, we created a separate branch from the last tag with mandatory merging back into the main branch — the fix was no longer lost in the next release.
Six months later, merge conflicts dropped to one or two per release instead of eight to ten, version preparation takes a few hours instead of three days, and urgent fixes in the running environment became isolated cases instead of every third release. There were no rollbacks due to changes lost during merging.
What to do if the team already has its own branching scheme?
An existing branching scheme is not a reason to break it.
If the repository already has accumulated history, release tags are in place, and a dozen working branches are alive, you cannot rewrite everything for Git Flow: the connection between commits and built artifacts would be broken, and deployment would stall while conflicts are sorted out.
Therefore we change the rules only for new changes — old branches quietly live out their lives under the previous logic.
The transition point is fixed explicitly. The last stable tag becomes the base of the main branch; a development branch is created from it, and from then on a working branch lives no longer than one task: a short branch means a predictable merge.
During migration, we close long-lived branches to direct pushes so that changes do not arrive in them bypassing checks.
We do not rewrite shared history: no rebasing or squashing of already merged commits, only new merges on top. This preserves rollback to the previous tag and does not break engineers' local copies — after updating, they do not have to rebuild working branches.
We separately agree on a freeze: while the transition is underway, long branches are not cut in half.
We check the transition by measurable criteria: time from task branch cut to inclusion in a release, number of merge conflicts, share of changes that passed checks before entering the main branch.
If conflicts do not grow and the release has not shifted, the scheme has taken hold — it can then be tightened: branch protection, mandatory checks, tags for every release.
Technical concerns: branches, merges, and rollbacks
The most common concerns before switching to Git Flow are not about the branching scheme but about edge cases: long-lived branches, unfinished work by the release date, access, and rollback. Each of them is addressed by regulations, not by engineers' heroics.
Long-lived branches diverge from the mainline over time: the longer a release branch lives separately, the larger the merge conflicts.
We limit its lifetime to the release window and synchronize it with the main branch daily — conflicts are resolved in small portions, not in one rush before release.
Unfinished work does not block the release: incomplete changes remain in short separate branches and wait for the next cycle. The release includes what is ready, not what happened to mature by the date.
We build repository access on least privilege: the main branch and release branches are protected, changes reach them only through a verifiable merge, and direct edits are closed. We grant permissions by role and revoke them when tasks change.
Rolling back changes is a routine operation. A release is marked with a tag, so returning to the previous state takes minutes and does not rewrite history: a specific set of changes is reverted, and other branches are not affected.
An urgent fix is needed in the middle of a release — what to do?
The fix goes out as a short branch from the mainline, passes checks, and lands in the nearest release. We do not touch the current release branch — otherwise unverified changes will get into it.
Can a release be rolled back while keeping some changes?
Yes. The release is rolled back entirely by tag, after which the necessary changes are moved to a separate branch and pass the usual checks. Repository history remains readable.
Let's discuss your branching and release scheme
To come back with an estimate and a plan rather than generalities, it is enough to bring to the discussion what already exists in your repository. Below is what helps most quickly: these points reveal both the scope of work and the bottlenecks that make releases slow.
- Current branching scheme: which branches live long, which are created per task, where version work is done. It shows where heavy merges occur and why fixes get lost.
- Release procedure: who creates the release branch and when, how fixes get into it, how the release returns to the main branch. This is a key part of the transition to Git Flow.
- Version numbering and tags: by what rule numbers are assigned, where the tag is applied, and whether it matches what is actually running in the environment.
- Checks before merging: which checks are mandatory, who approves a merge, how long the author of a change usually waits for a response.
- Roles and access rights: who merges into protected branches, who publishes tags, how keys are issued and stored, whether there is shared access.
- Build and artifact storage: what builds the result, where it is stored, and whether the same build can be unambiguously obtained by tag.
- Rollback procedure: how you return to the previous release and how long it takes now.
Based on these answers, we describe the scope of work, the implementation order, and the points where the scheme needs to change without stopping releases. Describe the task — we will come back with an estimate and a plan for your repository.







