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.
Why do branches exist?
Section titled “Why do branches exist?”A branch gives the Payment feature a separate line of development:
mainA ── B ── C \ D ── E feature-paymentAandBare existing commits.feature-paymentbegins from commitB.- Payment work continues through commits
DandE. maincan advance toCwith 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
.gitdirectory.
Instead, Git creates another reference that initially points to an existing commit.
Before creating the branch:
main ↓ C1Immediately after creating feature-payment:
mainfeature-payment ↓ C1Both 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.
What is main?
Section titled “What is main?”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 lineRather 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.
Is the default branch always called main?
Section titled “Is the default branch always called main?”No. Many modern repositories use:
mainMany older repositories still use:
masterOther projects may use:
developtrunkproductionThe 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:
git config --global init.defaultBranch main--globalstores 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.defaultBranchis the configuration key for the initial branch name used bygit init.mainis the value assigned to that key.- This affects future repositories created with
git init. - It does not rename branches in existing repositories.
mainandmasterdo 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 branch is a movable reference
Section titled “A branch is a movable reference”A local branch is a named reference to a commit. Suppose the history is:
C1 ── C2 ── C3 ↑ mainHere, 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 ↑ mainC1,C2, andC3did not move.- The name
mainmoved forward to point toC4. - 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.
What is HEAD?
Section titled “What is HEAD?”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.
HEADnormally 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
HEADrefers 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.
How Git stores local branches
Section titled “How Git stores local branches”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/mainThis 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/mainTogether, this represents the chain:
HEAD → refs/heads/main → commit a3f8c21HEAD 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 historyViewing local branches
Section titled “Viewing local branches”Run:
git branchExample output:
feature-payment* maingit branchwithout another argument lists local branches.- The asterisk
*marks the current branch and is not part of the branch name. - In this example,
HEADrefers tomain. - Remote branches are not listed by default.
To display only the current branch name:
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:
mainThis command prints no branch name during a detached HEAD, a state not
covered in this lesson.
Viewing branch-tip commits
Section titled “Viewing branch-tip commits”Run:
git branch -vExample:
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 -vshows the tip of each branch. It does not display the complete history of each branch.
To inspect the overall structure briefly:
git log --oneline --decorate --graph --allIllustrative output:
* 91bc044 (feature-payment) feat: add payment service| * a3f8c21 (HEAD -> main) fix: reject empty login password|/* 30ab210 create authentication project--onelinedisplays a compact commit line.--decorateshows branch andHEADlabels.--graphvisualizes commit relationships.--allincludes 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:
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/mainrepresents the locally recorded last-known state of the remote branch — it is not a live connection to the remote server.git branch -adoes not automatically rungit fetch.- The displayed remote-tracking information may be outdated until a fetch occurs.
Creating a branch without switching
Section titled “Creating a branch without switching”Assume the learner is currently on main:
HEAD → main → C3Run:
git branch feature-paymentGit creates feature-payment at the commit currently referenced by HEAD:
main → C3feature-payment → C3HEAD → mainBoth 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
HEADto the new branch. - Upload the branch to a server.
Switching to an existing branch
Section titled “Switching to an existing branch”To switch to the new branch:
git switch feature-paymentAfter switching:
main → C3feature-payment → C3HEAD → feature-paymentOr equivalently:
HEAD → feature-payment → C3- The current branch becomes
feature-payment. - No new commit is created merely by switching.
mainstill points to the same commit.- Future commits will advance
feature-paymentwhile 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:
HEADso 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.
Switching with uncommitted changes
Section titled “Switching with uncommitted changes”Before switching, run:
git statusUncommitted 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 → C3feature-payment → C3If 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 → C4feature-payment → C3Different 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.phpPlease 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 statusbefore 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.
Creating and switching in one command
Section titled “Creating and switching in one command”Use the modern command:
git switch -c feature-paymentgit 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 → C3After:
main → C3feature-payment → C3HEAD → feature-paymentIt does not create a new commit. Both branches still point to the same commit until new work is committed.
Create, switch, edit, stage, and commit
Section titled “Create, switch, edit, stage, and commit”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.A commit advances only the current branch
Section titled “A commit advances only 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:
git add app/Services/PaymentService.phpgit commit -m "feat: add payment service"Git creates a new commit and advances only the current branch:
C4 ↑ feature-payment ↑ HEAD /C1 ── C2 ── C3 ↑ mainfeature-paymentnow points toC4.mainstill points toC3.C4hasC3as its parent.HEADstill identifiesfeature-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.
Returning to main
Section titled “Returning to main”Run:
git switch mainThe conceptual state becomes:
C4 ↑ feature-payment /C1 ── C2 ── C3 ↑ main ↑ HEADHEADnow refers tomain.- The Index and tracked Working Tree files are updated to represent
main. PaymentService.phpmay disappear from the current Working Tree if it does not exist inmain.- The Payment code was not erased — it remains reachable through
feature-payment. - It has not been integrated into
mainyet.
Applying the urgent Login fix on main
Section titled “Applying the urgent Login fix on main”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:
git add app/Services/AuthService.phpgit 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.
maincontains the Login hotfix.- Both lines share earlier history.
- Neither new commit automatically contains the other.
- Combining the branches (merging) is a later topic.
git switch versus git checkout
Section titled “git switch versus git checkout”Older Git tutorials commonly use:
git checkout maingit checkout -b feature-paymentThese commands remain valid and common. Modern branch-focused equivalents are:
git switch maingit 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,-bcreates 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 switchfocuses on changing branches.git restorefocuses on restoring file content.git checkouthistorically performs multiple jobs, which can be confusing for beginners.git checkouthas not been removed and is not incorrect — learners will still see it in older documentation and real projects.
Prefer
git switchfor branches andgit restorefor files in new beginner workflows. Learn to recognizegit checkoutbecause it remains widely used.
A complete git restore lesson is outside the scope of this lesson.
Branch naming
Section titled “Branch naming”Clear names describe the work:
feature-paymentfix-login-validationrefactor-auth-serviceVague names should be avoided:
testnewbranch1changes- Branch names should communicate intent.
- Naming conventions vary between teams.
- Prefixes such as
feature-,fix-, or forms such asfeature/paymentare conventions. - Git does not require any particular branch-naming convention.
Renaming a local branch
Section titled “Renaming a local branch”Rename a local branch by giving Git the old and new names:
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:
git branch -m feature-paymentThis 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.
Deleting a local branch safely
Section titled “Deleting a local branch safely”The currently checked-out branch cannot normally be deleted. Switch to another branch first:
git switch mainThen delete safely:
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 -dtries 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.
Why can safe deletion fail?
Section titled “Why can safe deletion fail?”The developer switches to main:
git switch mainThen attempts:
git branch -d feature-paymentGit may refuse:
error: the branch 'feature-payment' is not fully mergedhint: If you are sure you want to delete it, runhint: 'git branch -D feature-payment'feature-paymentmay 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.
Inspect before forced deletion
Section titled “Inspect before forced deletion”Before forcing deletion, inspect commits reachable from feature-payment
but not from main:
git log --oneline main..feature-paymentExample:
91bc044 feat: add payment servicemain..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.
Forced branch deletion
Section titled “Forced branch deletion”Uppercase -D is the shortcut for --delete --force:
git branch -D feature-paymentIt can remove the local branch reference even if Git considers its work unmerged.
What happens when a branch is deleted?
Section titled “What happens when a branch is deleted?”Before deletion:
feature-payment → C4main → C3After deletion:
main → C3
No feature-payment branch reference points to C4Deleting 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”git branch -d feature-paymentdeletes 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.
Complete practical walkthrough
Section titled “Complete practical walkthrough”Step 1: inspect the current state
Section titled “Step 1: inspect the current state”git branch --show-currentgit statusExpected current branch:
mainStep 2: create and switch to the Payment branch
Section titled “Step 2: create and switch to the Payment branch”git switch -c feature-paymentState:
main → C2feature-payment → C2HEAD → feature-paymentStep 3: create Payment work
Section titled “Step 3: create Payment work”Create app/Services/PaymentService.php:
<?php
namespace App\Services;
class PaymentService{ public function charge(int $amount): bool { return $amount > 0; }}Then:
git add app/Services/PaymentService.phpgit commit -m "feat: add payment service"State:
main → C2feature-payment → C3HEAD → feature-paymentStep 4: return to main
Section titled “Step 4: return to main”git switch mainTracked 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.
Step 5: fix Login
Section titled “Step 5: fix Login”Update app/Services/AuthService.php and run:
git add app/Services/AuthService.phpgit commit -m "fix: reject empty login password"State:
feature-payment → Payment commitmain → Login fix commitHEAD → mainStep 6: inspect the branch positions
Section titled “Step 6: inspect the branch positions”git branch -vgit log --oneline --decorate --graph --allStep 7: demonstrate safe-deletion protection
Section titled “Step 7: demonstrate safe-deletion protection”As an educational example only:
git branch -d feature-paymentGit may refuse for the reasons explained earlier. The following commands are shown for learning purposes and are not executed against a real project:
git log --oneline main..feature-paymentgit branch -D feature-paymentForced deletion remains dangerous and should only follow careful inspection.
Command summary
Section titled “Command summary”| 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 |
Common mistakes
Section titled “Common mistakes”Thinking a branch copies the project
Section titled “Thinking a branch copies the project”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”git branch feature-paymentcreates the branch but keeps HEAD on the current branch. Use
git switch -c feature-payment to create and switch.
Thinking main is automatically protected
Section titled “Thinking main is automatically protected”main is technically an ordinary branch. Stability comes from team
practices and repository settings.
Thinking a new repository must use main
Section titled “Thinking a new repository must use main”The initial name depends on configuration and convention.
Switching without checking local changes
Section titled “Switching without checking local changes”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.
Thinking git checkout is invalid
Section titled “Thinking git checkout is invalid”It remains valid, but git switch and git restore separate its common
jobs more clearly.
Using -D whenever -d refuses
Section titled “Using -D whenever -d refuses”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.
Key takeaway
Section titled “Key takeaway”Commits form project history. A branch is a movable name pointing to a commit.
HEADnormally identifies the current branch. Creating a branch adds another reference; switching updatesHEAD, the Index, and the Working Tree; and committing advances only the current branch.