Branching and Merging

Creating branches, fast-forward vs merge commits, and resolving a merge conflict.

Why branches exist

A branch is just a movable pointer to a commit — nothing more. Creating a branch doesn't copy any files; it creates a lightweight named reference so you can develop a feature, fix a bug, or experiment without touching the stable line of history (conventionally called main). This cheapness is deliberate: Git is designed around branching and merging constantly, unlike older systems where branching was an expensive, rare operation.

Bash
$ git branch
* main

The * marks the branch you currently have checked out.

Creating and switching branches

Bash
# Create a new branch (does not switch to it)
$ git branch feature/user-login

# Switch to it
$ git checkout feature/user-login

# Create and switch in one step (the traditional way)
$ git checkout -b feature/user-login

# Create and switch in one step (the modern, less overloaded way)
$ git switch -c feature/user-login

git checkout historically did double duty — switching branches and restoring individual files — which caused enough confusion that Git introduced git switch (for branches) and git restore (for files) as clearer, split-out alternatives. Both checkout -b and switch -c are in common use; you'll see either in the wild.

Bash
$ git switch -c feature/user-login
Switched to a new branch 'feature/user-login'

# ... make changes, commit as usual ...
$ git add .
$ git commit -m "Add login form validation"

Each branch's commits are independent until merged — main doesn't see this commit at all yet.

Merging

Once a feature branch is ready, git merge brings its changes into another branch (typically switching back to main first):

Bash
$ git switch main
$ git merge feature/user-login

What actually happens depends on whether main has moved since the branch was created.

Fast-forward merge

If main hasn't changed at all since feature/user-login branched off, Git can simply move main's pointer forward to the tip of the feature branch — no new commit is needed, because there's nothing to reconcile:

Plaintext
Before:              After fast-forward:
main                  
 |                     
 A---B                 A---B---C---D
      \                            ^
       C---D                 main, feature/user-login
    feature/user-login
Bash
$ git merge feature/user-login
Updating a1b2c3d..d4e5f6a
Fast-forward
 login.php | 12 ++++++++++++
 1 file changed, 12 insertions(+)

Merge commit (three-way merge)

If main has moved forward with other commits in the meantime, Git can't just slide the pointer — it creates a new merge commit with two parents, combining both histories:

Plaintext
Before:                    After merge commit:
main                       
 |                          
 A---B---E                  A---B---E---F  (merge commit)
      \                          \       /
       C---D                      C-----D
    feature/user-login
Bash
$ git merge feature/user-login
Merge made by the 'recursive' strategy.
 login.php | 12 ++++++++++++
 1 file changed, 12 insertions(+)

Git opens your editor for the merge commit message unless you pass -m directly. The result is a history that honestly shows both lines of development actually happened in parallel, rather than pretending the feature was built after E rather than alongside it.

Resolving a merge conflict

A conflict happens when both branches changed the same lines of the same file, and Git genuinely can't guess which version you want. Git stops the merge partway through and marks the file with conflict markers for you to resolve by hand.

Given config.php changed differently on both branches:

PHP
<<<<<<< HEAD
$timeout = 30;
=======
$timeout = 60;
>>>>>>> feature/user-login
  • Everything between <<<<<<< HEAD and ======= is what's currently on your branch (main, since that's what's checked out).
  • Everything between ======= and >>>>>>> feature/user-login is what's coming in from the branch being merged.

You resolve it by editing the file to the version you actually want — removing the markers entirely — for example, deciding 60 is correct:

PHP
$timeout = 60;

Then finish the merge like a normal commit:

Bash
$ git add config.php
$ git commit -m "Merge feature/user-login, resolve timeout conflict"

git status during a conflict tells you exactly which files still need resolving:

Bash
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   config.php

If a merge goes badly wrong, git merge --abort cleanly backs out and returns you to the state right before the merge started.

Common mistakes

  • Leaving conflict markers (<<<<<<<, =======, >>>>>>>) in a file after a conflict — the file still has valid-looking code but the markers themselves break syntax; always search for them before committing.
  • Assuming every merge is a "clean" fast-forward — once a team is working in parallel, merge commits are normal and expected, not a sign something went wrong.
  • Deleting a branch (git branch -d) before confirming it was actually merged — Git will warn and refuse if it detects unmerged commits, but -D (capital) forces deletion and skips that safety check.

Interview questions

Q: What's the difference between a fast-forward merge and a merge commit? A fast-forward happens when the target branch hasn't diverged at all — Git just moves its pointer forward to the feature branch's tip, with no new commit created. A merge commit is created when both branches have new commits since they diverged; Git creates a new commit with two parents to combine both histories, preserving the fact that they developed in parallel.

Q: How do you resolve a merge conflict? Git marks the conflicting sections in each affected file with <<<<<<<, =======, and >>>>>>> markers, showing both versions of the conflicting lines. You edit the file to the content you actually want (removing the markers entirely), then git add the resolved file and git commit to complete the merge.

Q: What's the difference between git checkout -b and git switch -c? Both create a new branch and switch to it immediately — functionally, for this purpose, they're equivalent. git switch is the newer, more focused command introduced to separate branch-switching from git checkout's older, overloaded behavior (which also handled restoring individual files), making intent clearer in scripts and for newcomers.