Branches and Merging: Parallel Lines of Work
📑 On this page
- A concrete example: a feature and a hotfix
- What a Git branch actually is
- The current branch and HEAD
- Creating a branch
- Fast-forward integration
- Merge commits
- Why merge conflicts occur
- Resolving a conflict
- Semantic conflicts
- Merging frequently
- Rebasing
- Squashing
- Branch naming and purpose
- Protecting important branches
- Branches are not complete isolation
- Reducing conflict cost
- Deleting merged branches
- Knowledge check
- The one idea to remember
Software teams often need to work on several things at once.
One developer is building checkout, another is correcting an urgent security flaw, and a third is preparing the next release. Editing one shared folder in sequence would make that work painfully slow.
A branch names a line of history so work can progress independently until the team intentionally integrates it.
Branches reduce immediate interference, but they do not remove the need to reconcile design decisions and overlapping code.
A concrete example: a feature and a hotfix
The main branch contains version 4.2 of an application.
A feature branch starts adding a redesigned checkout. Before it is ready, production needs an urgent authentication fix.
The team can:
- apply and release the fix from the main line,
- continue the checkout work separately,
- bring the security fix into the feature branch, and
- merge the completed checkout later.
Each history advances without forcing unfinished feature code into the emergency release.
What a Git branch actually is
In Git, a branch is a lightweight movable reference to a commit.
When a new commit is created on that branch, the reference moves to the new commit. The earlier commits remain part of history.
The branch does not contain a separate physical copy of every file. Git reconstructs the working tree associated with the selected commit.
The current branch and HEAD
Git uses HEAD to indicate the currently checked-out position.
Normally, HEAD refers to a branch, and that branch advances when you commit. In a detached state, HEAD points directly to a commit; new work can become difficult to find unless a branch or tag is created for it.
Understanding the current position prevents changes from being recorded on the wrong line of work.
Creating a branch
A new branch usually begins at an existing commit.
At that moment, the new and original branch names point to the same place. They diverge only after new commits appear on one or both branches.
Choosing the correct starting point matters. A release fix based on experimental future code may be impossible to deploy safely to the current production version.
Fast-forward integration
Suppose the main branch has not changed since a feature branch was created.
When the feature is integrated, Git can simply move the main branch reference forward to the feature's latest commit. This is a fast-forward because no divergent histories need reconciliation.
The commits remain the same; only the branch reference advances.
Merge commits
If both branches have new commits, Git can create a merge commit with both histories as parents.
The merge commit records that the two lines were combined. It may contain automatic integration results plus any human conflict resolutions.
Some teams preserve these commits to show feature boundaries. Others prefer a more linear history. The right policy depends on how the team uses history.
Why merge conflicts occur
A conflict occurs when Git cannot safely choose one combined result.
Typical causes include:
- two branches edit the same lines differently,
- one branch deletes a file another edits,
- files are renamed in incompatible ways, or
- generated files change independently.
A conflict is not Git failing. It is Git refusing to invent a decision when human intent is ambiguous.
Resolving a conflict
Conflict resolution requires understanding both changes.
A developer should:
- inspect the common ancestor,
- understand each branch's intended behavior,
- edit the file into a coherent combined state,
- remove conflict markers,
- run relevant tests and checks, and
- record the resolution.
Blindly choosing "ours" or "theirs" can silently discard necessary behavior.
Semantic conflicts
Git can merge text successfully while the combined software is still wrong.
For example, one branch renames a permission and another adds code using the old permission. The edits may affect different lines, so no textual conflict appears, but the integrated behavior fails.
These semantic conflicts are why merged code still needs review and testing.
Merging frequently
Long-lived branches accumulate distance from the main line.
The more code and assumptions diverge, the harder integration becomes. Bringing main-branch changes into a feature regularly can reveal incompatibilities while their context is fresh.
Frequent integration is helpful, but unstable changes should not be merged merely to satisfy a schedule.
Rebasing
Rebasing replays commits onto a different base, creating new commits with new identifiers.
It can make a feature appear to have started from the latest main branch and produce a linear history. However, because it rewrites commit ancestry, rebasing shared work requires coordination.
Rebase and merge are history-shaping tools, not competing moral positions.
Squashing
Squashing combines several commits into fewer commits during or before integration.
It can turn temporary "fix typo" and "address review" commits into a clean feature record. Excessive squashing can also erase useful steps and authorship context.
The useful question is what history future maintainers will need, not whether every branch must follow one ritual.
Branch naming and purpose
Clear names help humans understand active work:
feature/order-historyfix/duplicate-chargerelease/4.3
Names are temporary navigation aids. Commit messages and review records must still explain the work after the branch is deleted.
Protecting important branches
Hosting platforms can protect main or release branches by requiring:
- pull-request review,
- passing automated checks,
- signed commits,
- current integration with the target,
- or restricted direct pushes.
Protection turns team policy into an enforceable workflow, though emergency procedures still need deliberate design.
Branches are not complete isolation
Code can be isolated while shared resources are not.
Two branches may still interact through:
- one shared development database,
- a common test environment,
- package registries,
- feature-flag configuration,
- or external services.
Safe parallel work considers those dependencies as well as Git history.
Reducing conflict cost
Teams reduce merge pain when they:
- keep branches focused and reasonably short-lived,
- communicate before changing shared interfaces,
- avoid unrelated mass formatting,
- split ownership boundaries clearly,
- integrate main-line changes regularly,
- and test the final combined state.
Architecture and communication influence conflict frequency as much as Git commands do.
Deleting merged branches
After integration, a feature branch name can usually be deleted without deleting its commits.
The merged commits remain reachable through the target branch. Removing stale names keeps the repository easier to navigate and clarifies which work is still active.
Unmerged branches should be checked before deletion because unique commits may otherwise become difficult to recover.
Knowledge check
- What does a Git branch point to?
- When can an integration be fast-forwarded?
- Why should conflict resolution consider intent rather than only text?
- What is a semantic conflict?
- Why does rebasing shared history require coordination?
The one idea to remember
Branches let histories diverge temporarily; merging or rebasing integrates them later. Git can combine compatible text, but people must resolve ambiguous intent and verify that the resulting behavior works as one system.