Introduction
Branching in Git isn’t just a feature. It’s the backbone of how modern teams collaborate on code without stepping on each other’s toes. If you’ve ever had a teammate accidentally overwrite your changes or struggled to juggle multiple features at once, you already know why branches matter.
This guide walks through how branching actually works, when to use it, and the patterns that make team collaboration smoother. No fluff, just what you need to work effectively with branches in real projects.
Why Branches Exist
Git branches let you diverge from the main codebase to work on something independently. Think of it like creating a parallel version of your project where you can experiment, build features, or fix bugs without affecting what’s already stable.
Without branches, every commit would go straight to the main code. That means incomplete features, broken tests, and experimental code would mix with production-ready work. Branches give you isolation.
# Create a new branch
git branch feature/user-authentication
# Switch to it
git checkout feature/user-authentication
# Or do both in one command
git checkout -b feature/user-authentication
When you create a branch, Git essentially creates a pointer to the current commit. As you make new commits, that pointer moves forward, while other branches stay where they were. This is why branching in Git is fast and lightweight.
Common Branching Strategies
Different teams use different branching models depending on their workflow. Here are the ones you’ll encounter most often:
Git Flow
Git Flow uses multiple long-lived branches with specific purposes:
main(ormaster): Production-ready codedevelop: Integration branch for featuresfeature/*: Individual feature branchesrelease/*: Preparing a new production releasehotfix/*: Urgent fixes for production
# Start a new feature
git checkout -b feature/add-payment-gateway develop
# Finish the feature
git checkout develop
git merge --no-ff feature/add-payment-gateway
git branch -d feature/add-payment-gateway
Git Flow works well for projects with scheduled releases, but it can feel heavy for continuous deployment setups.
GitHub Flow
GitHub Flow is simpler. You have main and feature branches. That’s it.
# Create feature branch from main
git checkout -b fix/button-alignment main
# Work on it, push when ready
git push origin fix/button-alignment
# Open a pull request, get it reviewed, merge to main
This model assumes main is always deployable. Once your PR is merged, you deploy. It’s popular with teams that ship often.
Trunk-Based Development
Trunk-based development keeps branches short-lived (usually less than a day). Developers work directly on main or create tiny branches that merge quickly.
# Small changes might go directly to main
git checkout main
git pull
# make changes
git commit -m "Update API endpoint URL"
git push
This approach requires good automated testing and feature flags to hide incomplete work. It’s fast but needs discipline.
Naming Conventions
Consistent branch names make it easier to understand what’s happening in your repository at a glance.
| Pattern | Example | When to Use |
|---|---|---|
feature/description | feature/user-profile | New functionality |
fix/description | fix/login-timeout | Bug fixes |
hotfix/description | hotfix/security-patch | Critical production fixes |
refactor/description | refactor/auth-module | Code improvements without changing behavior |
chore/description | chore/update-dependencies | Maintenance tasks |
Some teams include ticket numbers:
git checkout -b feature/PROJ-1234-add-search
This links branches directly to your project management tool.
💡 Tip: Avoid generic names like bug-fix or new-feature. Be specific enough that someone can understand the purpose without reading the commits.
Working with Remote Branches
Your local branches exist only on your machine until you push them.
# Push your branch to remote
git push -u origin feature/user-authentication
# The -u flag sets up tracking, so future pushes can just be:
git push
To see all branches (local and remote):
git branch -a
Fetch updates from remote without merging:
git fetch origin
Pull changes from a remote branch:
git pull origin develop
⚠️ Warning: git pull is actually git fetch + git merge. If you want more control, fetch first, review changes, then merge manually.
Merging vs Rebasing
When you’re ready to integrate your branch, you have two main options: merge or rebase.
Merge
Merging creates a new commit that combines two branches.
git checkout main
git merge feature/user-authentication
This preserves the complete history, including when branches diverged and converged. The downside is that merge commits can make the history noisy if you’re merging often.
# Fast-forward merge (no merge commit)
git merge --ff-only feature/user-authentication
# Always create a merge commit (even if fast-forward is possible)
git merge --no-ff feature/user-authentication
Rebase
Rebasing moves your commits to the tip of another branch, rewriting history to make it linear.
git checkout feature/user-authentication
git rebase main
Your commits are replayed on top of main, creating a cleaner history. But rebasing changes commit hashes, which can cause problems if others are working on the same branch.
📌 Note: Never rebase branches that have been pushed and shared with others. Only rebase local branches or branches you’re certain no one else is using.
When to Use Each
- Merge when you want to preserve context about when work happened in parallel
- Rebase when you want a linear history and are working on a feature branch alone
- Merge for integrating long-lived branches like
developintomain - Rebase for keeping your feature branch up to date with
main
Handling Merge Conflicts
Conflicts happen when Git can’t automatically reconcile changes in two branches. You’ll see something like this:
Auto-merging src/auth.js
CONFLICT (content): Merge conflict in src/auth.js
Automatic merge failed; fix conflicts and then commit the result.
Open the conflicted file:
:<<<<<<< HEAD
function login(username, password) {
return api.post('/auth/login', { username, password });
}
=======
function login(email, password) {
return api.post('/v2/auth/login', { email, password });
}
:>>>>>>> feature/update-auth
The section between <<<<<<< HEAD and ======= is what’s in your current branch. Below that, until >>>>>>>, is what’s coming from the other branch.
Edit the file to resolve it:
function login(email, password) {
return api.post('/v2/auth/login', { email, password });
}
Then stage and commit:
git add src/auth.js
git commit -m "Resolve merge conflict in auth.js"
💡 Tip: Use a merge tool if you’re dealing with complex conflicts. Run git mergetool to open your configured merge tool (like VS Code, Meld, or KDiff3).
Keeping Your Branch Updated
Long-lived feature branches can drift far from main, making merges painful. Stay synced:
# Regularly pull main into your feature branch
git checkout feature/user-authentication
git pull origin main
Or with rebase:
git checkout feature/user-authentication
git fetch origin
git rebase origin/main
Some teams do this daily. Others do it before opening a pull request. Find what works for your team, but don’t let branches go weeks without syncing.
Deleting Branches
Once a branch is merged, clean it up:
# Delete local branch
git branch -d feature/user-authentication
# Delete remote branch
git push origin --delete feature/user-authentication
The -d flag is safe—it prevents deletion if the branch hasn’t been merged. Use -D to force delete if you’re sure:
git branch -D feature/abandoned-experiment
Many teams automate this. GitHub, GitLab, and Bitbucket can delete branches automatically after PR merges.
Pull Request Best Practices
Pull requests (or merge requests) are where branches come together for review. Here’s what makes them effective:
-
Keep them focused: One feature or fix per PR. Large PRs are harder to review and more likely to introduce bugs.
-
Write clear descriptions: Explain what changed and why. Link to relevant tickets or issues.
## Changes
- Added email validation to registration form
- Updated error messages for better UX
## Testing
- Unit tests added for validation logic
- Manually tested registration flow
Fixes #234
-
Review your own PR first: Before requesting review, check the diff yourself. You’ll often catch issues you missed.
-
Respond to feedback constructively: Code review isn’t personal. It’s about making the code better.
-
Use draft PRs early: Open a draft PR to show work in progress and get early feedback on your approach.
⚠️ Warning: Don’t commit directly to main if your team uses PRs. Even small fixes should go through the same process. It maintains consistency and catches mistakes.
Common Mistakes
Committing to the Wrong Branch
It happens. You’re working away and realize you’re on main instead of your feature branch.
# Move uncommitted changes to a new branch
git checkout -b feature/correct-branch
# Or if you already committed:
git checkout main
git checkout -b feature/correct-branch
git checkout main
git reset --hard HEAD~1 # Remove the last commit from main
git checkout feature/correct-branch
Forgetting to Pull Before Starting Work
Always pull the latest changes before creating a new branch:
git checkout main
git pull
git checkout -b feature/new-work
Starting from an outdated main means you’re building on old code, and you’ll deal with more conflicts later.
Keeping Branches Too Long
Feature branches that live for weeks accumulate drift from main. The longer they exist, the harder they are to merge. Break large features into smaller, mergeable pieces.
Not Communicating About Shared Branches
If multiple people are working on the same branch, coordinate. Rebasing or force pushing can destroy others’ work.
Practical Workflow Example
Here’s a realistic flow for a feature:
# Start from updated main
git checkout main
git pull
# Create feature branch
git checkout -b feature/add-dark-mode
# Work on it, commit regularly
git add src/theme.js
git commit -m "Add theme toggle component"
git add src/App.js
git commit -m "Integrate theme toggle in header"
# Push to remote
git push -u origin feature/add-dark-mode
# Keep it updated with main
git fetch origin
git rebase origin/main
# Resolve any conflicts, then:
git rebase --continue
git push --force-with-lease
# Open PR, get reviewed, merge
# Then clean up
git checkout main
git pull
git branch -d feature/add-dark-mode
--force-with-lease is safer than --force because it won’t overwrite remote changes you don’t have locally.
Branch Protection Rules
Most Git hosting platforms let you protect branches. Common rules:
- Require pull request reviews before merging
- Require status checks (CI/CD tests) to pass
- Prevent force pushes
- Prevent deletion
- Require signed commits
Set these on your main and develop branches to prevent accidental mistakes.
When to Create a Branch
Every change should happen on a branch, with a few exceptions:
- New features: Always
- Bug fixes: Yes
- Refactoring: Yes
- Documentation updates: Usually yes, but tiny typo fixes might go direct to
maindepending on your team - Dependency updates: Yes, especially major version bumps
- Experiments: Definitely, and delete if they don’t pan out
If you’re unsure, create a branch. It’s easier to delete an unnecessary branch than to untangle commits that shouldn’t have been on main.
Stashing Changes
Sometimes you need to switch branches but have uncommitted work:
# Save your changes temporarily
git stash
# Switch branches and do other work
git checkout main
# Come back and restore your changes
git checkout feature/user-authentication
git stash pop
You can stash multiple times:
git stash list
git stash apply stash@{1}
Wrapping Up
Good branching practices come down to a few principles:
- Keep branches focused and short-lived
- Name them clearly
- Stay synced with
main - Use pull requests for visibility and review
- Clean up after merging
The specific strategy matters less than consistency. Pick a workflow, document it, and follow it as a team. That’s what turns branching from a feature you use into a foundation for smooth collaboration.