Skip to content

Git Branches: Why, How, and Essential Commands

Imagine the project currently has a working Login feature. The latest code considered ready by the team is on main.

The team now needs two days to implement a Payment feature. While the Payment work is still unfinished, an urgent Login bug appears and requires a ten-minute fix.

If both tasks happen on the same line of development, the urgent Login fix can become mixed with unfinished Payment code.

A branch gives the Payment feature a separate line of development:

main
A ── B ── C
\
D ── E
feature-payment
  • A and B are existing commits.
  • feature-payment begins from commit B.
  • Payment work continues through commits D and E.
  • main can advance to C with the Login hotfix.
  • The unfinished Payment work does not need to be included in the Login fix.
  • Both lines of work still belong to the same Git repository.
  • The completed feature can be integrated later.
  • The integration process (merging) is taught in a later lesson.

The practical benefits:

  • Work on a large feature can continue independently.
  • An urgent fix can be prepared without unfinished feature code.
  • Developers can experiment without immediately changing the primary branch.
  • Different pieces of work can progress in parallel.
  • The project does not need to be duplicated into multiple folders.

A Git branch is not a copied project folder. It is a lightweight movable name that points to a commit.

One repository, multiple lines of development

Section titled “One repository, multiple lines of development”

Creating feature-payment does not create a second project directory such as:

laravel-shop-payment-copy/

It also does not duplicate:

  • Every source file.
  • Every blob.
  • Every tree.
  • Every previous commit.
  • The complete .git directory.

Instead, Git creates another reference that initially points to an existing commit.

Before creating the branch:

main
↓
C1

Immediately after creating feature-payment:

main
feature-payment
↓
C1

Both branch names initially point to C1. Git reuses the existing stored objects and commit history — this is why creating a Git branch is normally fast and inexpensive.

main is a commonly used name for the primary development branch. Teams often use it as the branch from which they:

  • Prepare releases.
  • Deploy code.
  • Create feature branches.
  • Apply urgent fixes.
  • Begin new work.

Its importance comes from:

  • The team’s workflow.
  • Development conventions.
  • Automated tests.
  • Code review.
  • Protection configured on a hosting platform.
Technical meaning:
main = a branch name pointing to a commit
Team meaning:
main = the branch chosen as the primary development line

Rather than saying “main is always safe,” it is more accurate to say that the team tries to keep main safe and ready through its workflow.

No. Many modern repositories use:

main

Many older repositories still use:

master

Other projects may use:

develop
trunk
production

The initial branch name can depend on the user’s Git configuration, the command used to initialize the repository, the hosting provider, and the project’s existing conventions — not every new repository automatically starts with main.

The preferred initial branch name for future repositories can be configured:

Terminal window
git config --global init.defaultBranch main
  • --global stores the setting in the current user’s Git configuration, so it applies across that user’s repositories unless a more specific setting overrides it.
  • init.defaultBranch is the configuration key for the initial branch name used by git init.
  • main is the value assigned to that key.
  • This affects future repositories created with git init.
  • It does not rename branches in existing repositories.
  • main and master do not have different technical powers — both are simply branch names.

Before the first commit, the initial branch can have a name without yet pointing to a commit. This state is called an unborn branch, and it is not covered in detail in this lesson.

A local branch is a named reference to a commit. Suppose the history is:

C1 ── C2 ── C3
↑
main

Here, main points to C3. When the developer is on main and creates another commit, Git creates a new commit whose parent is C3, then advances the main reference:

C1 ── C2 ── C3 ── C4
↑
main
  • C1, C2, and C3 did not move.
  • The name main moved forward to point to C4.
  • The branch does not contain separate copies of all commits.
  • It provides a named route to the commit at its tip and the history reachable from it.

Commits form the project history. A branch is a movable name that points to one position in that history.

HEAD represents the current checkout position. During normal branch work:

HEAD

The current checkout position.

Branch

main

The currently checked-out branch.

Commit

C4

The snapshot the current work is based on.

  • HEAD normally refers to the currently checked-out branch.
  • The branch refers to a commit.
  • That commit identifies the snapshot on which the current work is based.
  • When a new commit is created, the current branch advances, and because HEAD refers to that branch, it follows the branch forward.

Rather than the inaccurate absolute statement “HEAD always points directly to the latest commit,” it is more accurate to say that during normal branch work, HEAD refers to the current branch, and the branch refers to a commit.

HEAD can sometimes point directly to a commit instead of a branch, in a state called detached HEAD. That state is outside the scope of this lesson.

This section briefly connects to the existing lesson about the .git directory rather than repeating it.

A local branch named main is logically represented by a reference named:

refs/heads/main

This reference points to the object ID of the commit at the tip of main. Illustrative content:

a3f8c21c9d7e...

HEAD commonly contains a symbolic reference such as:

ref: refs/heads/main

Together, this represents the chain:

HEAD → refs/heads/main → commit a3f8c21
HEAD
identifies the current branch reference
refs/heads/main
identifies the commit at the tip of main
commit
points to the project snapshot and its parent history

Run:

Terminal window
git branch

Example output:

feature-payment
* main
  • git branch without another argument lists local branches.
  • The asterisk * marks the current branch and is not part of the branch name.
  • In this example, HEAD refers to main.
  • Remote branches are not listed by default.

To display only the current branch name:

Terminal window
git branch --show-current

--show-current asks git branch to print only the name of the currently checked-out branch rather than listing every local branch.

Example:

main

This command prints no branch name during a detached HEAD, a state not covered in this lesson.

Run:

Terminal window
git branch -v

Example:

feature-payment 91bc044 feat: add payment service
* main a3f8c21 fix: reject empty login password

-v adds the abbreviated commit ID at the tip of each local branch and the subject line of that commit.

git branch -v shows the tip of each branch. It does not display the complete history of each branch.

To inspect the overall structure briefly:

Terminal window
git log --oneline --decorate --graph --all

Illustrative output:

* 91bc044 (feature-payment) feat: add payment service
| * a3f8c21 (HEAD -> main) fix: reject empty login password
|/
* 30ab210 create authentication project
  • --oneline displays a compact commit line.
  • --decorate shows branch and HEAD labels.
  • --graph visualizes commit relationships.
  • --all includes history reachable through all local references.

The full behavior of git log is covered in its dedicated lesson.

Viewing local and remote-tracking branches

Section titled “Viewing local and remote-tracking branches”

Run:

Terminal window
git branch -a

-a is the short form of --all. It includes both local branches and the remote-tracking references currently stored in the local repository.

Illustrative output:

* main
feature-payment
remotes/origin/main
remotes/origin/feature-payment
  • Local branches are displayed.
  • Remote-tracking references stored in the local repository are also displayed.
  • origin/main represents the locally recorded last-known state of the remote branch — it is not a live connection to the remote server.
  • git branch -a does not automatically run git fetch.
  • The displayed remote-tracking information may be outdated until a fetch occurs.

Assume the learner is currently on main:

HEAD → main → C3

Run:

Terminal window
git branch feature-payment

Git creates feature-payment at the commit currently referenced by HEAD:

main → C3
feature-payment → C3
HEAD → main

Both branch names point to C3, but HEAD still refers to main. So git branch feature-payment means: create the branch and remain on the current branch. It does not:

  • Copy the project.
  • Copy all existing files.
  • Create a new commit.
  • Switch to the new branch.
  • Move HEAD to the new branch.
  • Upload the branch to a server.

To switch to the new branch:

Terminal window
git switch feature-payment

After switching:

main → C3
feature-payment → C3
HEAD → feature-payment

Or equivalently:

HEAD → feature-payment → C3
  • The current branch becomes feature-payment.
  • No new commit is created merely by switching.
  • main still points to the same commit.
  • Future commits will advance feature-payment while it remains the current branch.

What really happens when switching branches?

Section titled “What really happens when switching branches?”

Switching does not only move HEAD. When switching to another branch, Git normally updates:

  • HEAD so it refers to the target branch.
  • The Index (Staging Area) to match the target branch’s checked-out state.
  • Tracked Working Tree files so they represent the target branch’s snapshot.

On main, the tracked project contains:

<?php
namespace App\Services;
class AuthService
{
public function login(string $email, string $password): bool
{
return true;
}
}

On feature-payment, a tracked file exists:

<?php
namespace App\Services;
class PaymentService
{
public function charge(int $amount): bool
{
return $amount > 0;
}
}

When switching between the branches, tracked files may appear, disappear, or change content — they change in the Working Tree to represent the selected branch’s snapshot.

  • Git is not downloading another project copy; it is checking out stored file versions.
  • A file is not necessarily deleted from repository history when it disappears from the current Working Tree.
  • Untracked files are not automatically deleted simply because the learner switches branches.

Before switching, run:

Terminal window
git status

Uncommitted changes belong to the current Working Tree and Index, not permanently to a branch. Until a commit records them, they can sometimes remain present after a switch and appear while another branch is current.

Three common cases explain the behavior:

Case 1: both branches point to the same commit

Section titled “Case 1: both branches point to the same commit”
main → C3
feature-payment → C3

If you edit auth.js and switch between these branches, Git usually has no different committed snapshot to check out. The unstaged or staged change can remain present after the switch.

Case 2: the branch tips differ, but the local edit can remain safely

Section titled “Case 2: the branch tips differ, but the local edit can remain safely”
main → C4
feature-payment → C3

Different tips alone do not force a refusal. If the target switch does not need to replace the locally edited content—or Git can preserve the change without losing information—the change may travel to the target branch.

This can be dangerous conceptually: the edit was started while main was current, but a later commit could accidentally record it on feature-payment.

Case 3: the target branch would overwrite the local edit

Section titled “Case 3: the target branch would overwrite the local edit”

Suppose auth.js has one committed version on the current branch, another committed version on the target branch, and an additional local edit. Git cannot simply replace the file with the target version without losing the local work, so the normal switch is refused.

The same protection applies to an untracked path when the target branch needs to write a tracked file at that path.

Illustrative error:

Your local changes to the following files would be overwritten by checkout:
app/Services/AuthService.php
Please commit your changes or stash them before you switch branches.

The safe choices are to commit a coherent change, discard it only when it is truly unwanted, or temporarily park it with git stash. Stashing is covered in the next lesson.

Review git status before switching so you understand the current Working Tree and Staging Area.

A beginner does not need to create a poor or incomplete commit solely to switch branches.

Use the modern command:

Terminal window
git switch -c feature-payment
git switch
Switch branches.
-c
Create a new branch before switching.
feature-payment
The new branch name.

The complete command means: create feature-payment at the current commit, then switch HEAD to feature-payment.

Before:

HEAD → main → C3

After:

main → C3
feature-payment → C3
HEAD → feature-payment

It does not create a new commit. Both branches still point to the same commit until new work is committed.

git branch feature-payment
Creates a branch reference.
No commit is created.
git switch feature-payment
Changes the current branch.
No commit is created.
Edit PaymentService.php
Changes the Working Tree.
No commit is created.
git add app/Services/PaymentService.php
Updates the Staging Area.
No commit is created.
git commit
Creates a new commit.
Advances the current branch.

After switching to feature-payment, create:

<?php
namespace App\Services;
class PaymentService
{
public function charge(int $amount): bool
{
if ($amount <= 0) {
return false;
}
return true;
}
}

Then run:

Terminal window
git add app/Services/PaymentService.php
git commit -m "feat: add payment service"

Git creates a new commit and advances only the current branch:

C4
↑
feature-payment
↑
HEAD
/
C1 ── C2 ── C3
↑
main
  • feature-payment now points to C4.
  • main still points to C3.
  • C4 has C3 as its parent.
  • HEAD still identifies feature-payment.
  • The new commit has not automatically changed main.
  • The commit remains local until pushed.
  • Conventional Commit formatting is optional and is not required by Git.

Run:

Terminal window
git switch main

The conceptual state becomes:

C4
↑
feature-payment
/
C1 ── C2 ── C3
↑
main
↑
HEAD
  • HEAD now refers to main.
  • The Index and tracked Working Tree files are updated to represent main.
  • PaymentService.php may disappear from the current Working Tree if it does not exist in main.
  • The Payment code was not erased — it remains reachable through feature-payment.
  • It has not been integrated into main yet.

Update:

<?php
namespace App\Services;
use InvalidArgumentException;
class AuthService
{
public function login(string $email, string $password): bool
{
if ($password === '') {
throw new InvalidArgumentException('Password is required');
}
return true;
}
}

Then:

Terminal window
git add app/Services/AuthService.php
git commit -m "fix: reject empty login password"

The history now diverges:

C4
↑
feature-payment
/
C1 ── C2 ── C3
\
C5
↑
main
↑
HEAD
  • The Payment branch contains the Payment commit.
  • main contains the Login hotfix.
  • Both lines share earlier history.
  • Neither new commit automatically contains the other.
  • Combining the branches (merging) is a later topic.

Older Git tutorials commonly use:

Terminal window
git checkout main
git checkout -b feature-payment

These commands remain valid and common. Modern branch-focused equivalents are:

Terminal window
git switch main
git switch -c feature-payment
Goal Modern focused command Older command
Switch to an existing branch git switch main git checkout main
Create and switch git switch -c feature-payment git checkout -b feature-payment
Restore a Working Tree file git restore AuthService.php git checkout -- AuthService.php
  • In git checkout -b feature-payment, -b creates the new branch before checking it out.
  • In git checkout -- AuthService.php, -- separates options or revisions from the file path. The command restores the file; it does not switch branches.
  • git switch focuses on changing branches.
  • git restore focuses on restoring file content.
  • git checkout historically performs multiple jobs, which can be confusing for beginners.
  • git checkout has not been removed and is not incorrect — learners will still see it in older documentation and real projects.

Prefer git switch for branches and git restore for files in new beginner workflows. Learn to recognize git checkout because it remains widely used.

A complete git restore lesson is outside the scope of this lesson.

Clear names describe the work:

feature-payment
fix-login-validation
refactor-auth-service

Vague names should be avoided:

test
new
branch1
changes
  • Branch names should communicate intent.
  • Naming conventions vary between teams.
  • Prefixes such as feature-, fix-, or forms such as feature/payment are conventions.
  • Git does not require any particular branch-naming convention.

Rename a local branch by giving Git the old and new names:

Terminal window
git branch -m feature-pay feature-payment
-m
Move or rename the branch reference and its configuration.
feature-pay
The existing local branch name.
feature-payment
The new local branch name.

If the branch being renamed is currently checked out, the old name can be omitted:

Terminal window
git branch -m feature-payment

This renames the branch reference; it does not rewrite commits or change their hashes. It also does not automatically rename a branch on a remote server.

Uppercase -M can overwrite an existing destination branch name. Do not use it merely to bypass a name collision; inspect both branch tips first.

The currently checked-out branch cannot normally be deleted. Switch to another branch first:

Terminal window
git switch main

Then delete safely:

Terminal window
git branch -d feature-payment
-d
Safe branch deletion.

Technically, Git requires the branch tip to be fully merged into its configured upstream branch, or into HEAD when no upstream is configured.

git branch -d tries to protect commits that may still be reachable only through the branch being deleted.

If deletion succeeds, Git may display:

Deleted branch feature-payment (was 91bc044).
  • This removes the local branch reference.
  • The abbreviated ID identifies the commit previously referenced by the branch.
  • It does not delete a remote branch.
  • It does not send anything to the server.

The developer switches to main:

Terminal window
git switch main

Then attempts:

Terminal window
git branch -d feature-payment

Git may refuse:

error: the branch 'feature-payment' is not fully merged
hint: If you are sure you want to delete it, run
hint: 'git branch -D feature-payment'
  • feature-payment may contain a commit that is not reachable from the safe-deletion target.
  • Removing the branch name may make that work harder to find.
  • Git refuses as a safety measure — the branch is not corrupt, and Git is not automatically merging anything.
  • The warning should be investigated, not blindly bypassed.

Before forcing deletion, inspect commits reachable from feature-payment but not from main:

Terminal window
git log --oneline main..feature-payment

Example:

91bc044 feat: add payment service
main..feature-payment
Show commits reachable from feature-payment
that are not reachable from main.

Full revision-range syntax is outside the scope of this lesson. If important commits appear, forced deletion may remove the clearest branch name leading to that work.

Uppercase -D is the shortcut for --delete --force:

Terminal window
git branch -D feature-payment

It can remove the local branch reference even if Git considers its work unmerged.

Before deletion:

feature-payment → C4
main → C3

After deletion:

main → C3
No feature-payment branch reference points to C4

Deleting a branch removes the branch reference. It does not directly erase the commit objects or project files at that exact moment.

The commit may remain reachable through another branch, a tag, a remote-tracking reference, another commit relationship, or the reflog for a limited period. If no reference keeps a commit reachable, Git may eventually remove the unreachable object during garbage collection — reflog recovery and garbage collection are not covered in this lesson, and deleted work should never be assumed permanently recoverable.

Local branch deletion does not delete a remote branch

Section titled “Local branch deletion does not delete a remote branch”
Terminal window
git branch -d feature-payment

deletes the local branch only. It does not delete origin/feature-payment from the remote server. Remote branch deletion requires a separate remote operation and belongs in a later lesson.

Terminal window
git branch --show-current
git status

Expected current branch:

main

Step 2: create and switch to the Payment branch

Section titled “Step 2: create and switch to the Payment branch”
Terminal window
git switch -c feature-payment

State:

main → C2
feature-payment → C2
HEAD → feature-payment

Create app/Services/PaymentService.php:

<?php
namespace App\Services;
class PaymentService
{
public function charge(int $amount): bool
{
return $amount > 0;
}
}

Then:

Terminal window
git add app/Services/PaymentService.php
git commit -m "feat: add payment service"

State:

main → C2
feature-payment → C3
HEAD → feature-payment
Terminal window
git switch main

Tracked Working Tree files now represent main at C2. PaymentService.php may no longer appear in the current Working Tree, but its committed version remains reachable from feature-payment.

Update app/Services/AuthService.php and run:

Terminal window
git add app/Services/AuthService.php
git commit -m "fix: reject empty login password"

State:

feature-payment → Payment commit
main → Login fix commit
HEAD → main
Terminal window
git branch -v
git log --oneline --decorate --graph --all

Step 7: demonstrate safe-deletion protection

Section titled “Step 7: demonstrate safe-deletion protection”

As an educational example only:

Terminal window
git branch -d feature-payment

Git may refuse for the reasons explained earlier. The following commands are shown for learning purposes and are not executed against a real project:

Terminal window
git log --oneline main..feature-payment
git branch -D feature-payment

Forced deletion remains dangerous and should only follow careful inspection.

Goal Command
List local branches git branch
Show the current branch name git branch --show-current
Show each branch and its tip commit git branch -v
Show local and remote-tracking branches git branch -a
Create a branch without switching git branch feature-payment
Switch to an existing branch git switch feature-payment
Create and switch in one step git switch -c feature-payment
Older switch command git checkout feature-payment
Older create-and-switch command git checkout -b feature-payment
Rename a local branch git branch -m feature-pay feature-payment
Safely delete a local branch git branch -d feature-payment
Force-delete a local branch git branch -D feature-payment
Inspect unique feature commits git log --oneline main..feature-payment
Visualize all branch history git log --oneline --decorate --graph --all

A branch is a reference to a commit. Creating it does not copy all files or Git objects.

Creating a branch and assuming Git switched to it

Section titled “Creating a branch and assuming Git switched to it”
Terminal window
git branch feature-payment

creates the branch but keeps HEAD on the current branch. Use git switch -c feature-payment to create and switch.

main is technically an ordinary branch. Stability comes from team practices and repository settings.

The initial name depends on configuration and convention.

Switching can update tracked Working Tree files. Run git status first.

Thinking every uncommitted change blocks switching

Section titled “Thinking every uncommitted change blocks switching”

Git may allow safe changes to remain. It refuses when the switch would overwrite or conflict with local work.

Thinking different branch tips always block local changes

Section titled “Thinking different branch tips always block local changes”

Branches can point to different commits and Git may still preserve the local edits. The deciding issue is whether checking out the target would lose or overwrite those edits—not whether the two branch tips are equal.

Thinking git branch -a contacts the server

Section titled “Thinking git branch -a contacts the server”

It displays locally known references and does not fetch automatically.

It remains valid, but git switch and git restore separate its common jobs more clearly.

The refusal is a safety warning. Inspect unique commits before forcing deletion.

Thinking branch deletion immediately destroys commits

Section titled “Thinking branch deletion immediately destroys commits”

The branch reference is removed. Commit objects are not necessarily erased immediately, but unreachable work must not be considered permanently safe.

Commits form project history. A branch is a movable name pointing to a commit. HEAD normally identifies the current branch. Creating a branch adds another reference; switching updates HEAD, the Index, and the Working Tree; and committing advances only the current branch.