Branching and Merging: The Complete Guide
Why Branches Exist
Imagine you're building a website. It's live, it works, and users are visiting it every day. Now you need to add a new feature — a contact form. The problem is, building the form will take a few days, and while you're in the middle of it, the code is in a half-finished, broken state. If you're working directly on the main codebase, the live site is at risk.
This is the exact problem branches solve.
A branch is an independent line of development. When you create a branch, you're making a copy of your project at that point in time where you can work freely — adding features, fixing bugs, experimenting — without affecting the main codebase. When your work is done and tested, you merge it back in. The main project was never at risk.
Branches are lightweight in Git. Creating one takes a fraction of a second. Switching between them is nearly instant. There's no performance penalty for having many branches. This means you can — and should — create branches for everything: features, bug fixes, experiments, even quick one-line changes.
Setting Up a Practice Project
Let's create a project to experiment with throughout this article:
mkdir git-branching-practice
cd git-branching-practice
git init
Create some initial files:
echo "<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Welcome to My Website</h1>
<p>This is the homepage.</p>
</body>
</html>" > index.html
echo "# My Website" > README.md
echo "body { font-family: Arial; }" > style.css
git add .
git commit -m "Initial commit: add homepage, README, and stylesheet"
We now have a repository with one commit on the main branch. Let's start branching.
Understanding the main Branch
When you create a new Git repository, Git automatically creates a default branch called main (in older versions of Git, this was called master). This is just a regular branch — there's nothing technically special about it. But by convention, main represents the stable, production-ready version of your project.
You can see your current branch at any time:
git branch
Output:
* main
The asterisk (*) tells you which branch you're currently on. Right now, main is the only branch and you're on it.
Creating a Branch
Let's say you want to add a contact page to the website. Instead of working directly on main, you'll create a branch for this feature:
git branch contact-page
This creates a new branch called contact-page. Let's verify:
git branch
Output:
contact-page
* main
Two branches now exist: main and contact-page. But notice the asterisk is still on main. Creating a branch doesn't automatically switch to it — it just creates the pointer. Think of it like putting a bookmark in a book. The bookmark exists, but you haven't turned to that page yet.
What Actually Happens When You Create a Branch?
Under the hood, a branch in Git is just a lightweight pointer to a specific commit. When you created contact-page, Git created a new pointer that points to the same commit you're currently on. Both main and contact-page point to the exact same place right now. They'll diverge as soon as you make commits on one but not the other.
Switching Branches
To start working on the contact page, you need to switch to the new branch:
git switch contact-page
Output:
Switched to branch 'contact-page'
Now verify:
git branch
Output:
* contact-page
main
The asterisk has moved. You're now on contact-page. Every commit you make from here will be recorded on this branch, not on main.
The Shortcut: Create and Switch in One Step
In practice, you'll almost always want to create a branch and immediately switch to it. There's a shortcut for that:
git switch -c new-branch-name
The -c flag means "create." This is the command you'll use 90% of the time. For example, if we hadn't created the branch already, we could have done:
git switch -c contact-page
A Note About git checkout
In older tutorials, you'll see git checkout -b branch-name used to create and switch branches. This still works, but git switch was introduced in Git 2.23 to replace it for branch-related operations. git switch is clearer and less error-prone because git checkout was overloaded — it handled both branch switching and file restoration, which confused many beginners. Use git switch for branches.
Working on a Branch
Now let's build the contact page. Remember, you're on the contact-page branch, so none of these changes will affect main.
Create a new file:
echo "<!DOCTYPE html>
<html>
<head>
<title>Contact Us</title>
</head>
<body>
<h1>Contact Us</h1>
<form>
<label>Name:</label>
<input type='text' name='name'>
<label>Email:</label>
<input type='email' name='email'>
<label>Message:</label>
<textarea name='message'></textarea>
<button type='submit'>Send</button>
</form>
</body>
</html>" > contact.html
git add contact.html
git commit -m "Add contact page with form"
Now add a link to the contact page from the homepage:
echo "<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Welcome to My Website</h1>
<p>This is the homepage.</p>
<a href='contact.html'>Contact Us</a>
</body>
</html>" > index.html
git add index.html
git commit -m "Add contact page link to homepage"
Let's check the log:
git log --oneline
Output:
e5f6g7h (HEAD -> contact-page) Add contact page link to homepage
d4e5f6g Add contact page with form
a1b2c3d (main) Initial commit: add homepage, README, and stylesheet
Notice how contact-page has three commits, while main is still sitting at the initial commit. The branches have diverged.
Switching Back to main
Now let's switch back to main and see what it looks like:
git switch main
Check the files:
ls
Output:
README.md index.html style.css
The contact.html file is gone. Open index.html and the contact link isn't there either. Your homepage is exactly as it was before — clean, unchanged, unbroken.
This is the magic of branches. The contact page work exists safely on the contact-page branch, completely invisible to main. You could have ten branches with ten different features in progress, and main stays pristine.
Switch back to see the contact page reappear:
git switch contact-page
ls
Output:
README.md contact.html index.html style.css
Everything's back. Git swaps your entire working directory to match whichever branch you're on.
Simulating Parallel Work
In real projects, work happens on multiple branches simultaneously. Let's simulate that. Switch back to main and create a different branch for a separate task:
git switch main
git switch -c update-styles
Now improve the stylesheet:
echo "body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}" > style.css
git add style.css
git commit -m "Improve stylesheet with better colors and spacing"
Now we have three branches, each with different content:
git log --oneline --graph --all
Output:
* f6g7h8i (HEAD -> update-styles) Improve stylesheet with better colors and spacing
| * e5f6g7h (contact-page) Add contact page link to homepage
| * d4e5f6g Add contact page with form
|/
* a1b2c3d (main) Initial commit: add homepage, README, and stylesheet
This ASCII graph shows the branching visually. Both contact-page and update-styles branched off from the same starting point (main), and each has its own commits. This is how real teams work — multiple people, multiple branches, all in parallel.
Merging: Bringing It All Together
Your contact page is done. The styles are updated. It's time to bring everything back together into main. This is called merging.
The rule for merging is simple: switch to the branch you want to merge INTO, then merge the other branch. You're always pulling changes in, not pushing them out.
Fast-Forward Merge
Let's start by merging the style updates into main:
git switch main
git merge update-styles
Output:
Updating a1b2c3d..f6g7h8i
Fast-forward
style.css | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
Git performed a fast-forward merge. This happens when the branch you're merging has commits that sit directly ahead of your current branch — there's no divergence, no competing changes. Git simply moves the main pointer forward to match update-styles. No new commit is created because there's nothing to reconcile.
Think of it like a highway: if main is at mile marker 1 and update-styles is at mile marker 2, Git just moves main up to mile marker 2. Straightforward.
Check the log:
git log --oneline
Output:
f6g7h8i (HEAD -> main, update-styles) Improve stylesheet with better colors and spacing
a1b2c3d Initial commit: add homepage, README, and stylesheet
Both main and update-styles now point to the same commit.
Three-Way Merge
Now let's merge the contact page. This one is different because main has moved forward (it now includes the style changes), while contact-page branched off from the original commit. The two branches have diverged — they each have changes the other doesn't.
git merge contact-page
Git opens your text editor with a default merge commit message:
Merge branch 'contact-page'
Save and close the editor. Git creates a special merge commit — a commit with two parents that ties the two branches together. This is called a three-way merge because Git uses three reference points to combine the changes: the common ancestor (where the branches diverged), the tip of main, and the tip of contact-page.
Check the log with the graph:
git log --oneline --graph
Output:
* g7h8i9j (HEAD -> main) Merge branch 'contact-page'
|\
| * e5f6g7h (contact-page) Add contact page link to homepage
| * d4e5f6g Add contact page with form
* | f6g7h8i (update-styles) Improve stylesheet with better colors and spacing
|/
* a1b2c3d Initial commit: add homepage, README, and stylesheet
You can see the two lines of development coming together into the merge commit. This is the complete story of your project: two features developed in parallel, both merged into main.
Handling Merge Conflicts
In the previous examples, Git merged everything automatically because the changes didn't overlap. But what happens when two branches modify the same lines in the same file? Git can't decide which version to keep, so it asks you to resolve the conflict manually.
Let's create a conflict on purpose so you can see exactly what happens and how to fix it.
Setting Up the Conflict
Create two branches that both modify the homepage heading:
git switch -c feature-a
Edit index.html — change the heading:
echo "<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Welcome to My Amazing Website</h1>
<p>This is the homepage.</p>
<a href='contact.html'>Contact Us</a>
</body>
</html>" > index.html
git add index.html
git commit -m "Update heading to 'My Amazing Website'"
Now switch back to main and create a different branch that changes the same heading differently:
git switch main
git switch -c feature-b
Edit the same line with a different change:
echo "<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Welcome to My Awesome Website</h1>
<p>This is the homepage.</p>
<a href='contact.html'>Contact Us</a>
</body>
</html>" > index.html
git add index.html
git commit -m "Update heading to 'My Awesome Website'"
Now merge feature-a into main first (this will work fine):
git switch main
git merge feature-a
This fast-forwards cleanly. Now try to merge feature-b:
git merge feature-b
Output:
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
Git found a conflict. Both branches changed the same line, and Git doesn't know which version you want. Let's see what git status says:
git status
Output:
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
Reading Conflict Markers
Open index.html in your editor. You'll see something like this:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<<<<<<< HEAD
<h1>Welcome to My Amazing Website</h1>
=======
<h1>Welcome to My Awesome Website</h1>
>>>>>>> feature-b
<p>This is the homepage.</p>
<a href='contact.html'>Contact Us</a>
</body>
</html>
Git has inserted conflict markers to show you exactly where the problem is:
<<<<<<< HEAD— The start of the conflict. Everything between here and=======is what's on your current branch (main, which now includes feature-a's changes).=======— The divider between the two versions.>>>>>>> feature-b— Everything between=======and here is what's on the incoming branch (feature-b).
Your job is to choose which version to keep — or combine them into something new — and then remove the conflict markers entirely.
Resolving the Conflict
You have three options:
- Keep the current branch's version (Amazing)
- Keep the incoming branch's version (Awesome)
- Write something completely new that combines or replaces both
Let's say you decide to combine them. Edit index.html to remove the conflict markers and write the final version:
echo "<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Welcome to My Amazing & Awesome Website</h1>
<p>This is the homepage.</p>
<a href='contact.html'>Contact Us</a>
</body>
</html>" > index.html
Now tell Git the conflict is resolved by staging the file:
git add index.html
And complete the merge with a commit:
git commit -m "Merge feature-b: combine heading text from both branches"
Done. The conflict is resolved, the merge is complete, and your history reflects exactly what happened.
Aborting a Merge
If you're in the middle of a conflicted merge and you decide you're not ready to deal with it, you can back out entirely:
git merge --abort
This returns everything to the state it was in before you ran git merge. No changes, no conflict markers, no mess. You can always come back and try the merge again later.
Deleting Branches
After a branch has been merged, it's served its purpose. The commits are now part of main, so the branch pointer is just clutter. Clean it up:
git branch -d contact-page
git branch -d update-styles
git branch -d feature-a
git branch -d feature-b
The -d flag deletes a branch, but only if it's been fully merged. This is a safety check — Git won't let you accidentally delete a branch with unmerged work.
If you want to force-delete a branch that hasn't been merged (because you're abandoning the work intentionally), use uppercase -D:
git branch -D abandoned-experiment
After cleaning up, check your branches:
git branch
Output:
* main
Clean and tidy. All the work is in main, and the temporary branches are gone.
Branch Naming Conventions
As your projects grow, good branch names become essential. You'll have dozens of branches over a project's lifetime, and cryptic names like test2 or johns-branch are meaningless to everyone — including future you.
Here are the most common naming conventions used by professional teams:
| Prefix | Purpose | Example |
|---|---|---|
feature/ |
New features | feature/contact-form |
bugfix/ |
Bug fixes | bugfix/login-redirect |
hotfix/ |
Urgent production fixes | hotfix/payment-crash |
refactor/ |
Code cleanup with no behavior change | refactor/auth-module |
docs/ |
Documentation updates | docs/api-endpoints |
test/ |
Adding or fixing tests | test/user-registration |
chore/ |
Maintenance tasks | chore/update-dependencies |
Some teams also include ticket numbers: feature/JIRA-1234-contact-form. The format doesn't matter as much as consistency — pick a convention and stick with it across the team.
A few rules for branch names:
- Use lowercase letters and hyphens: feature/contact-form, not Feature/Contact Form
- Keep them short but descriptive
- No spaces (use hyphens instead)
- No special characters except /, -, and _
Listing and Inspecting Branches
Here are useful commands for working with branches day to day:
List All Local Branches
git branch
Shows all local branches. The current branch is marked with an asterisk.
List All Branches (Local and Remote)
git branch -a
Includes remote-tracking branches (from GitHub, GitLab, etc.). Useful when collaborating with a team.
See the Last Commit on Each Branch
git branch -v
Output:
* main g7h8i9j Merge feature-b: combine heading text
Shows the branch name, the short commit ID, and the commit message for the latest commit on each branch.
See Which Branches Have Been Merged
git branch --merged
Lists branches that have been fully merged into your current branch. These are safe to delete.
git branch --no-merged
Lists branches with unmerged work. Be careful deleting these — they contain commits that haven't been merged yet.
Real-World Branching Workflow
Here's a typical workflow that professional developers follow every day. Once you internalize this pattern, it becomes second nature:
- Start on main. Make sure it's up to date.
git switch main git pull # if working with a remote (covered later in the series) - Create a feature branch.
git switch -c feature/user-profile - Do your work. Make commits as you go — small, focused, with clear messages.
# ... edit files ... git add . git commit -m "Add user profile page layout" # ... edit more files ... git add . git commit -m "Add profile picture upload functionality" - Switch back to main and merge.
git switch main git merge feature/user-profile - Delete the branch.
git branch -d feature/user-profile - Repeat for the next task.
This pattern — branch, work, merge, delete — keeps your repository clean, your main branch stable, and your work organized. It also sets you up perfectly for collaboration: when you start working with remote repositories and pull requests (covered later in the series), this is the exact flow you'll use.
Tips for Effective Branching
Keep Branches Short-Lived
The longer a branch lives, the more it diverges from main, and the harder the merge becomes. Aim to merge branches within a few days, not weeks. If a feature is too big for that, break it into smaller pieces and merge each piece separately.
Commit Before Switching
Always commit (or stash — covered in Part 7) your work before switching branches. If you switch with uncommitted changes, those changes will carry over into the other branch, which leads to confusion. Git will warn you if switching would overwrite uncommitted changes, but it's best to commit first as a habit.
Merge main Into Your Branch Regularly
If you're working on a long-running branch, periodically merge main into your feature branch to stay up to date:
git switch feature/my-feature
git merge main
This brings your branch up to date with the latest changes from the rest of the team and lets you resolve conflicts incrementally — a few small conflicts along the way are much easier than one massive conflict at the end.
Don't Be Afraid to Create Branches
Branches are free. They're fast to create, fast to switch, and fast to delete. There's no penalty for having many of them. If you're about to try something risky — even a one-line experiment — create a branch. It takes two seconds and gives you a safety net.
Quick Reference
| Command | What It Does |
|---|---|
git branch |
List all local branches |
git branch branch-name |
Create a new branch (without switching) |
git switch branch-name |
Switch to an existing branch |
git switch -c branch-name |
Create a new branch and switch to it |
git merge branch-name |
Merge a branch into the current branch |
git merge --abort |
Abort a conflicted merge and return to pre-merge state |
git branch -d branch-name |
Delete a branch (only if merged) |
git branch -D branch-name |
Force-delete a branch (even if unmerged) |
git branch -v |
List branches with their latest commit |
git branch -a |
List all branches including remote-tracking ones |
git branch --merged |
List branches that have been merged into current branch |
git branch --no-merged |
List branches with unmerged work |
git log --oneline --graph --all |
Visual graph of all branches and commits |