Git Branching for PHP Projects: Features, Fixes and Releases

Git branching for PHP projects is the skill that separates developers who protect their live site from those who don’t. Working directly on the main branch is fine when you’re the only developer and the project is small. The moment you have multiple features in progress, a live site to protect, or more than one person touching the code, it stops working.

Branches solve this. Each branch is an isolated copy of the code where you can build, test, and break things without touching what’s live. This guide covers Git branching specifically for PHP projects – the commands, the workflow, and the patterns that actually work on real projects.

What Is a Branch?

A branch is a separate line of development. When you create a branch, Git gives you an independent workspace that shares the same history up to that point but diverges from there. Changes on your branch don’t affect the main branch until you deliberately merge them.

main branch:     A - B - C - D
                              \
feature branch:                E - F - G

Commits E, F, G exist only on the feature branch. Main branch stays at D – stable, deployable, untouched – while you build the feature.

Creating and Switching Branches

# Create a new branch
git branch feature/user-authentication

# Switch to it
git checkout feature/user-authentication

# Or do both in one command - faster, used more often
git checkout -b feature/user-authentication

Output:

Switched to a new branch 'feature/user-authentication'

Modern Git also has git switch which does the same thing with clearer syntax:

# Modern equivalent of checkout
git switch -c feature/user-authentication

Either works. checkout -b is more common in documentation and tutorials because it’s been around longer. Both do the same thing.

Branch Naming Conventions

Consistent names make your branch list readable and tell anyone looking at the repo what’s in progress. The pattern that works for most PHP projects:

# Feature work
feature/user-authentication
feature/stripe-payment-integration
feature/product-search-filter
feature/rss-feed-parser

# Bug fixes
fix/login-redirect-loop
fix/null-pointer-session-handler
fix/curl-timeout-on-scraper

# Hotfixes for production (urgent)
hotfix/sql-injection-search-form
hotfix/broken-checkout-redirect

# Releases
release/v1.2.0
release/v2.0.0-beta

The prefix tells you the type at a glance. The suffix describes the specific work. No spaces – use hyphens. Keep it short enough to read in a terminal without wrapping.

The Daily Git Branching PHP Workflow

The same sequence every time you start something new:

# Step 1: Make sure main is up to date
git checkout main
git pull

# Step 2: Create your branch from main
git checkout -b feature/email-notifications

# Step 3: Do your work - code, test, commit
git add src/Notifications/EmailNotifier.php
git commit -m "Add EmailNotifier class with SMTP support"

git add src/Notifications/EmailTemplate.php
git commit -m "Add email templates for registration and password reset"

git add tests/EmailNotifierTest.php
git commit -m "Add tests for EmailNotifier"

# Step 4: Push the branch to remote
git push -u origin feature/email-notifications

Output after push:

Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Writing objects: 100% (5/5), 2.14 KiB | 2.14 MiB/s, done.
To https://github.com/username/php-project.git
 * [new branch]      feature/email-notifications -> feature/email-notifications
Branch 'feature/email-notifications' set up to track remote branch.

Listing and Checking Branches

# List local branches
git branch

# List all branches including remote
git branch -a

# List branches with last commit info
git branch -v

# List branches already merged into current branch
git branch --merged

# List branches not yet merged
git branch --no-merged

Output of git branch -v:

  feature/email-notifications  3f7a9b2 Add tests for EmailNotifier
* feature/user-authentication  8c4d1e5 Add password hashing to registration
  main                         2a9f3b1 Release v1.1.0

The asterisk marks your current branch. The hash and message show the latest commit on each branch. Check this when you can’t remember which branch you’re on or what’s in progress.

Merging a Branch

When the feature is done and tested, merge it back into main:

# Switch to main first
git checkout main

# Make sure main is current
git pull

# Merge the feature branch
git merge feature/user-authentication

Output when merge is clean:

Updating 2a9f3b1..8c4d1e5
Fast-forward
 src/Auth/UserRegistration.php | 45 +++++++++++++++++++++
 src/Auth/PasswordHasher.php   | 23 +++++++++++
 2 files changed, 68 insertions(+)

“Fast-forward” means main hadn’t changed since you branched off it – Git just moved the pointer forward. Clean and simple.

Merge With a Commit (–no-ff)

Fast-forward merges don’t create a merge commit, which means the branch history disappears in the log. For feature branches you want to preserve in history:

git merge --no-ff feature/user-authentication -m "Merge feature/user-authentication into main"

Output:

Merge made by the 'ort' strategy.
 src/Auth/UserRegistration.php | 45 +++++++++++++++++++++
 src/Auth/PasswordHasher.php   | 23 +++++++++++
 2 files changed, 68 insertions(+)

Now git log --graph shows the branch clearly:

*   a1b2c3d Merge feature/user-authentication into main
|\
| * 8c4d1e5 Add password hashing to registration
| * 3f7a9b2 Add UserRegistration class
|/
* 2a9f3b1 Release v1.1.0

Handling Merge Conflicts

Conflicts happen when two branches modify the same lines of the same file. Git can’t decide which version to keep – it needs you to decide.

# This triggers a conflict
git checkout main
git merge feature/user-authentication

Output when conflict exists:

Auto-merging src/Auth/LoginController.php
CONFLICT (content): Merge conflict in src/Auth/LoginController.php
Automatic merge failed; fix conflicts and then commit the result.

Open the conflicting file. Git marks the conflict like this:

<?php
class LoginController
{
    public function login(Request $request)
    {
<<<<<<< HEAD
        // main branch version
        $user = User::findByEmail($request->email);
=======
        // feature branch version
        $user = User::where('email', $request->email)
                    ->with('roles')
                    ->first();
>>>>>>> feature/user-authentication
    }
}

The section between <<<<<<< HEAD and ======= is the current branch version. The section between ======= and >>>>>>> is the incoming branch version. Edit the file to keep what you want:

<?php
class LoginController
{
    public function login(Request $request)
    {
        // Resolved - keep the feature branch version with eager loading
        $user = User::where('email', $request->email)
                    ->with('roles')
                    ->first();
    }
}

Then stage the resolved file and commit:

git add src/Auth/LoginController.php
git commit -m "Merge feature/user-authentication - resolve login query conflict"

If the conflict is too messy and you want to start over:

git merge --abort

This puts everything back to where it was before the merge attempt. No damage done.

Deleting Branches

Merged branches are just dead weight after they’re merged. Delete them to keep the branch list readable:

# Delete a merged branch locally
git branch -d feature/user-authentication

# Delete the remote branch
git push origin --delete feature/user-authentication

# Force delete an unmerged branch (careful)
git branch -D feature/abandoned-experiment

Output:

Deleted branch feature/user-authentication (was 8c4d1e5).
To https://github.com/username/php-project.git
 - [deleted]         feature/user-authentication

The -d flag refuses to delete if the branch hasn’t been merged – a safety net against accidentally deleting work. Use -D only when you’re certain the work is either merged or genuinely abandoned.

Keeping a Branch Up to Date

Long-running feature branches drift from main as other work gets merged. Bring your branch up to date with the latest main before merging:

# Option 1: Merge main into your branch
git checkout feature/payment-integration
git merge main

# Option 2: Rebase onto main (cleaner history)
git checkout feature/payment-integration
git rebase main

The difference:

# After merge - history shows the merge commit
* Merge branch 'main' into feature/payment-integration
* Add Stripe webhook handler
* Add payment form validation
* main: Release v1.1.0

# After rebase - clean linear history
* Add Stripe webhook handler
* Add payment form validation
* main: Release v1.1.0

Rebase rewrites your branch commits on top of the latest main. The result is a cleaner history but rebase rewrites commit hashes – never rebase a branch other developers are working on. For personal feature branches rebase is fine and preferred. For shared branches use merge.

A Practical PHP Project Branching Pattern

This pattern works well for PHP projects with a live site:

main          - production-ready code, always deployable
develop       - integration branch, features merge here first
feature/*     - individual features branch from develop
hotfix/*      - urgent fixes branch from main directly

The workflow in practice:

# Start a new feature
git checkout develop
git checkout -b feature/pdf-invoice-generator

# Work, commit, push...

# Feature done - merge to develop for testing
git checkout develop
git merge --no-ff feature/pdf-invoice-generator

# Release - merge develop to main
git checkout main
git merge --no-ff develop
git tag -a v1.2.0 -m "Release v1.2.0"
git push --tags

# Production bug found - hotfix from main
git checkout main
git checkout -b hotfix/fix-payment-redirect

# Fix it, commit, merge to both main AND develop
git checkout main
git merge --no-ff hotfix/fix-payment-redirect

git checkout develop
git merge --no-ff hotfix/fix-payment-redirect

git branch -d hotfix/fix-payment-redirect

The hotfix merges to both main and develop so the fix doesn’t get lost when develop eventually merges to main again.

Checking Which Branch Contains a Commit

# Which branches contain a specific commit?
git branch --contains 3f7a9b2

# Which branches do NOT contain it?
git branch --no-contains 3f7a9b2

# Was this commit merged to main?
git branch main --contains 3f7a9b2

Output:

  feature/user-authentication
* main

Useful when you need to verify a fix actually made it to production or track down where a specific change lives.

Viewing Branch Differences

# What commits are on feature branch but not main?
git log main..feature/user-authentication --oneline

# What files changed between branches?
git diff main..feature/user-authentication --name-only

# Full diff between branches
git diff main..feature/user-authentication

Output of log comparison:

8c4d1e5 Add password hashing to registration
3f7a9b2 Add UserRegistration class with validation
7e2c8d4 Add users migration

Run this before merging to see exactly what you’re about to bring in.

Frequently Asked Questions

How many branches should a PHP project have at once?

As many as there are things actively being worked on – no more. One branch per feature, one per bug fix. The problem isn’t having many branches, it’s having branches that sit around for weeks without merging. Long-lived branches drift from main and cause painful conflicts later. Aim to merge and delete feature branches within a few days of starting them. If a feature takes weeks, break it into smaller branches that can merge independently.

Should I branch for every small bug fix?

Yes if you’re working on a live application. A one-line fix on main that introduces a regression can break production. A branch keeps the fix isolated until you’ve tested it. The cost of creating a branch is essentially zero – it’s two commands. The cost of pushing a bad fix to production is much higher.

What is the difference between merge and rebase?

Both integrate changes from one branch into another but they do it differently. Merge creates a new merge commit that records where two branches joined – history is preserved exactly as it happened. Rebase replays your commits on top of the target branch as if you had started from there – history looks linear and cleaner but the original branch structure is gone. Use merge for shared branches and integrating to main. Use rebase to update a personal feature branch with the latest main before merging.

Can I work on multiple branches at the same time?

You can only have one branch checked out at a time in a standard Git setup. To work on two features simultaneously, use git stash to save work in progress, switch branches, work on the other feature, then switch back and pop the stash. Alternatively git worktree lets you check out multiple branches in separate directories on the same machine – useful for comparing implementations side by side.

My branch is out of date with main. Should I merge or rebase?

Rebase if it’s your own feature branch and nobody else is working on it. It keeps the history clean and makes the eventual merge into main straightforward. Merge if the branch is shared with other developers – rebasing a shared branch rewrites commits that others have based their work on, causing serious confusion. The safe rule: only rebase branches that exist solely on your machine or that you own completely.


Summary

Git branching for PHP projects comes down to a consistent pattern:

  • Branch from main for every new feature or fix – never work directly on main for a live application
  • Name branches descriptivelyfeature/stripe-integration not new-stuff
  • Keep branches short-lived – merge and delete within days, not weeks
  • Update from main regularlygit rebase main on your feature branch to stay current
  • Use –no-ff for merging features – preserves branch history in the log
  • Hotfixes branch from main – and merge back to both main and develop

The next post in this series covers undoing mistakes in Git – when you commit to the wrong branch, commit sensitive data, need to revert a bad deployment, or want to undo the last N commits without losing your work. For the daily workflow that branching builds on top of, start with Git for PHP developers: setup and daily workflow.

For the full branching documentation see the official Git branching documentation.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top