Skip to main content

Git Branching & Merging

Branching is Git’s killer feature. Create branches in milliseconds, experiment freely, and merge with confidence.

Why Branching Matters

Git Branching

Parallel Development

Multiple features in progress simultaneously without interference

Safe Experimentation

Try ideas without risking the main codebase

Code Review

Review changes before merging to main

Clean History

Organize work into logical units

Understanding Branches

A branch is just a pointer to a commit. That is it. It is a 41-byte file containing a commit hash. This is why Git branches are so cheap to create — there is no copying of files, no duplicating directories. Creating a branch is like putting a sticky note on a page in a book: the book does not change, you just marked where you are.

Creating Branches

Switching Branches

Before switching branches: Commit or stash your changes! Git won’t let you switch if you have uncommitted changes that would be overwritten.

Branch Naming Conventions


Merging Branches

Fast-Forward Merge

When there are no new commits on the target branch:

Three-Way Merge

When both branches have new commits:

Resolving Merge Conflicts

Conflicts happen when the same lines are changed in both branches. They are not failures — they are Git honestly telling you, “I found changes in both branches that touch the same code, and I do not want to guess which one you intended.” Conflicts are normal, expected, and a sign that your team is actively working on the same codebase.

Example Conflict

Conflict Markers

  • <<<<<<< HEAD: Your current branch’s version
  • =======: Separator
  • >>>>>>> feature/redesign: Incoming branch’s version

Resolving

1

Open the conflicted file

2

Edit the file

Remove conflict markers and choose the correct version:
3

Stage the resolved file

4

Complete the merge

Conflict Resolution Tools


Deleting Branches


Branch Management

Viewing Branches

Renaming Branches


Common Branching Workflows

Feature Branch Workflow

Gitflow Workflow

Gitflow is designed for software with scheduled release cycles — mobile apps, desktop software, libraries with semantic versioning. It adds ceremony (more branches, more rules), so only use it when you genuinely need release branches.
Production gotcha: The most common Gitflow mistake is forgetting to merge hotfixes back to develop. This means the next release re-introduces the bug you just fixed in production. Automate this with CI or use a checklist.

Trunk-Based Development

Everyone commits to main (the “trunk”) or uses extremely short-lived branches (hours, not days). This is how Google, Meta, and most high-velocity SaaS teams operate. The key insight: merging frequently with small changes is far less risky than merging infrequently with large changes.
Prerequisite: Trunk-based development requires a robust CI pipeline and strong test coverage. Without automated tests catching regressions, merging to main multiple times per day means breaking main multiple times per day. Start with Feature Branch workflow and graduate to trunk-based when your CI and testing culture are mature.

Merge Strategies

Merge (Default)

Pros: Preserves complete history
Cons: Can create a messy history with many merge commits

Squash Merge

Pros: Clean main branch history — each feature is one commit
Cons: Loses individual commit history (the 47 “WIP” and “fix typo” commits disappear, which is usually a feature, not a bug)
When to squash: Squash merge is the most popular strategy for merging PRs at companies like GitHub, Shopify, and Stripe. The reasoning is simple — the main branch should tell the story of what shipped, not the messy journey of how it got built. Individual commit history is preserved in the PR itself if anyone needs it.

Rebase (Advanced)

Pros: Linear history
Cons: Rewrites history (don’t rebase public branches!)

Practical Examples

Example 1: Feature Development

Example 2: Handling Conflicts


Best Practices

  • Merge within days, not weeks
  • Reduces merge conflicts
  • Easier code review
feature/user-authentication
fix/login-timeout
my-branch
test

Troubleshooting

”Cannot switch branches - uncommitted changes”

This happens because Git refuses to overwrite your uncommitted work when switching branches — it is protecting you, not blocking you.

”Merge conflict in binary file”

Binary files (images, fonts, compiled assets) cannot be auto-merged because there is no meaningful “line-by-line diff.” You must choose one version or the other.
Production gotcha: Binary conflicts are common in repos with designers committing image assets. The best prevention is Git LFS (Large File Storage), which stores binaries outside the repo and reduces conflict frequency. Additionally, agree on a convention: “the branch being merged in wins for assets” — this eliminates the decision paralysis.

”Accidentally committed to wrong branch”

This happens more often than anyone admits. The fix is straightforward: cherry-pick the commit to the correct branch, then remove it from the wrong one.
Be careful: git reset --hard is destructive — it permanently removes the commit from the branch. Make sure you have successfully cherry-picked it to the correct branch first. If you are nervous, use git reset --soft HEAD~1 instead, which un-commits but keeps the changes staged.

Key Takeaways

  • Branches are lightweight pointers
  • Create branches freely for features, fixes, experiments
  • Merge conflicts are normal—learn to resolve them
  • Delete branches after merging
  • Choose a workflow that fits your team

Interview Deep-Dive

Strong Answer:
  • Squash merges collapse all commits from a feature branch into a single commit on main. The main branch tells the story of “what shipped” — one commit per feature/fix. This makes git log main clean, git bisect on main fast (fewer commits to search), and reverts trivial (git revert <single-sha>). Companies like GitHub, Shopify, and Stripe use this model. The individual commit history is preserved in the pull request UI if anyone needs it.
  • Regular merge commits preserve full branch history and add an explicit merge commit that records when and how branches were integrated. The benefit is complete traceability: you can see every intermediate step, every WIP commit, every fix-after-code-review commit. This matters in regulated environments where auditors want to see the exact sequence of changes. It also means git blame shows the original author of each line, not “the person who clicked merge.”
  • The trade-off comes down to: do you optimize for reading history (squash) or writing history (merge)? Squash merges require discipline in writing a good squash commit message (the default auto-generated message is usually garbage). Merge commits require discipline in keeping individual commits clean (otherwise main history is polluted with “WIP” and “fix typo”).
  • My recommendation depends on team culture. If the team writes clean, atomic commits with good messages, regular merges work well. If commits tend to be messy (most teams), squash merges produce a cleaner main branch. A third option — rebase-and-merge — replays the individual commits linearly on main without a merge commit, giving you clean history if the individual commits are clean.
Follow-up: With squash merges, git blame on main shows the person who merged the PR, not the original author. How do you deal with that?GitHub and GitLab preserve the original author as the commit author and set the merger as the committer in squash merges, but many tools only display the author. For teams that care about accurate attribution, the PR link in the commit message (e.g., (#456)) serves as the source of truth. I also recommend using the Co-authored-by trailer in squash commit messages to credit all contributors. For deep-dive blame, git log --follow -p file.txt and the PR history are more reliable than git blame alone.
Strong Answer:
  • First, I understand the three-way merge. There are three versions of the file: the common ancestor (base), our version (HEAD), and their version (the branch being merged). I view all three explicitly: git show :1:file.txt (base), git show :2:file.txt (ours), git show :3:file.txt (theirs). This gives me the full context that conflict markers alone do not.
  • Second, I examine the intent of each change. I run git log --oneline main..feature -- file.txt to see what commits from the feature branch touched this file, and git log --oneline feature..main -- file.txt for changes on main. Reading the commit messages tells me what each side was trying to accomplish.
  • Third, I use a visual merge tool. git mergetool opens a three-pane view showing base, ours, and theirs side by side. VS Code, IntelliJ, and Beyond Compare all support this. For a 100-line conflict, visual diffing is essential — raw conflict markers are unreadable at that scale.
  • Fourth, if the conflict is in code (not prose), I resolve it and immediately run the test suite. Tests catch logical errors in my resolution that I might miss visually. If tests pass, I am confident in the resolution.
  • Fifth, I make the resolution its own atomic step. I do not combine conflict resolution with other changes. The merge commit should contain only the resolution so that future reviewers can audit exactly what decisions were made during the merge.
Follow-up: After resolving, tests pass locally but the merge introduced a subtle logic bug that only appears under load. How could this happen?This happens when both branches changed the same logic independently in compatible but semantically conflicting ways. For example, branch A changes a function’s error handling, branch B changes the same function’s retry logic. Git sees no textual conflict (they modified different lines), so it auto-merges. But the combined behavior is wrong — the new error handling skips the new retry logic. This is called a “semantic merge conflict” and no tool can detect it automatically. The defense is thorough code review of merge commits, integration tests that exercise combined behavior, and in critical systems, requiring manual review of all auto-merged regions (not just conflicted ones) in the merge diff.
Strong Answer:
  • Feature Branch (the default for most teams): every feature gets a branch off main, merged via PR after review. Failure mode: long-lived feature branches that diverge significantly from main, leading to painful merge conflicts. Prevention: merge main into feature branches daily, keep branches under a week.
  • Gitflow: adds develop, release/*, and hotfix/* branches on top of feature branches. Best for software with versioned releases (mobile apps, desktop software, libraries). Failure mode: excessive ceremony. Teams spend more time managing branches than writing code. I have seen teams with 5 developers maintaining 4 concurrent release branches. Prevention: only adopt Gitflow if you genuinely ship versioned releases to different customers simultaneously.
  • Trunk-Based: everyone commits to main (or very short-lived branches that merge within hours). Used by Google, Meta, and most high-velocity SaaS companies. Failure mode: broken main. If the CI pipeline is slow or flaky, developers merge untested code. If there are no feature flags, half-built features ship to users. Prevention: fast CI (under 10 minutes), comprehensive test coverage, feature flags for in-progress work.
  • The pattern I see: most teams start with Feature Branch workflow. As the team grows and the deployment cadence increases, they either evolve toward trunk-based (SaaS products) or toward Gitflow (versioned products). The most common mistake is adopting a workflow that is too complex for the team’s maturity — a 3-person startup using full Gitflow is adding process overhead with no benefit.
Follow-up: Your team currently uses Gitflow and wants to migrate to trunk-based development. What prerequisites must be in place before the switch?Three prerequisites are non-negotiable. First, a CI pipeline that runs in under 10 minutes and is not flaky — if CI takes 30 minutes, developers will merge without waiting. Second, a feature flag system (LaunchDarkly, Unleash, or even a simple config file) so incomplete features can be deployed behind flags without exposing them to users. Third, a team culture of small, frequent commits — if developers are used to 2-week branches with 50 commits, they need to learn to break work into shippable increments. I would run both workflows in parallel during the transition: new features use trunk-based, existing long-lived branches finish in Gitflow, and after 2-3 months the team is fully migrated.

Next: Git Collaboration →