Skip to main content

Git Branch Management: Local, Remote, and Keeping Your Repo Clean

·6 mins
Table of Contents
Branch management is one of the most common sources of confusion and mistakes in team workflows. Branches pile up, remote tracking refs go stale, and people either delete the wrong thing or never clean up at all. This is the reference you’ll want to bookmark.

After a few months on a shared repository, the branch list starts to look like an archaeological dig. There are feature branches from tickets closed six months ago, a hotfix-DO-NOT-DELETE that nobody dares touch, and a dozen origin/feature-* tracking refs for branches that no longer exist on the remote. Understanding exactly how local branches, remote branches, and remote tracking refs relate to each other is the foundation of keeping this under control.

The Mental Model: Local vs Remote Tracking Branches
#

Before touching any delete commands, it helps to have a clear picture of what Git actually stores.

graph LR
    subgraph "Your Machine"
        A[main] -->|tracks| B[origin/main]
        C[feature-x] -->|tracks| D[origin/feature-x]
        E[old-branch]
    end
    subgraph "GitHub / GitLab"
        F[main]
        G[feature-x]
    end
    B -.->|last fetched state of| F
    D -.->|last fetched state of| G

origin/main is not the remote branch itself. It is your local copy of what the remote looked like the last time you ran git fetch. This distinction matters when you start pruning stale refs.

Listing Branches
#

Before you delete anything, know what you have.

# Local branches only
git branch

# Remote tracking branches only
git branch -r

# All branches (local + remote tracking)
git branch -a

# Branches already merged into main
git branch --merged main

# Branches not yet merged into main
git branch --no-merged main

The --merged and --no-merged filters are the safest way to identify what is safe to remove. Any branch in --merged main has already had its work incorporated, so the commits are not going anywhere even if you delete the branch pointer.

Deleting Local Branches
#

# Safe delete: refuses if branch is not fully merged
git branch -d feature/login

# Force delete: bypasses the merge check
git branch -D feature/login

The -d vs -D distinction is not arbitrary. -d is a guard: Git checks whether the tip of the branch you want to delete is reachable from HEAD or from the upstream tracking branch. If the answer is no, it refuses. -D overrides that check entirely.

Warning

git branch -D on unmerged work destroys commits that are not reachable from any other ref. The reflog will save you for 30 days, but after that the objects are garbage collected. Do not force-delete without double-checking git log feature/branch --oneline first.

You cannot delete the branch you are currently on. Switch to main (or any other branch) first:

git switch main
git branch -d feature/login

Deleting Remote Branches
#

There are two syntaxes that achieve the same thing:

# Modern, explicit syntax (preferred)
git push origin --delete feature/login

# Legacy colon syntax (still works, same effect)
git push origin :feature/login

The colon syntax comes from Git’s refspec format: src:dst. An empty src means “push nothing to this destination”, which deletes the ref on the remote. It is worth knowing because you will encounter it in older scripts and documentation.

Deleting a remote branch does not automatically remove the origin/feature/login tracking ref from your local repository. That requires pruning.

Pruning Stale Remote Tracking Refs
#

Once a branch is deleted on the remote, your local origin/feature/login ref is orphaned. It points to a remote that no longer exists. Git calls these “stale” tracking refs.

# Fetch and prune in one step (recommended for day-to-day use)
git fetch --prune

# Prune without fetching new updates
git remote prune origin

The difference: git fetch --prune contacts the remote, downloads any new refs, and removes any local tracking refs that are no longer on the remote. git remote prune origin only removes stale tracking refs – it does not download anything new. For most workflows, git fetch --prune is what you want.

You can also configure Git to prune automatically on every fetch:

git config --global fetch.prune true

With this setting, git fetch behaves like git fetch --prune everywhere.

Renaming Branches
#

Renaming a local branch is straightforward:

# Rename the current branch
git branch -m new-name

# Rename a specific branch (while on a different branch)
git branch -m old-name new-name

To propagate the rename to the remote, you need to push the new name and delete the old one:

git push origin new-name
git push origin --delete old-name

# Update the upstream tracking for your local branch
git branch --set-upstream-to=origin/new-name new-name

If your team has opened a pull request against the old branch name on GitHub or GitLab, renaming will require updating the PR’s base branch before deleting the old remote ref.

Protecting Branches from Accidental Deletion
#

Platform-level branch protection rules are your last line of defense against mistakes that git branch -D cannot protect against.

On GitHub: Settings > Branches > Branch protection rules. Check “Restrict deletions” to prevent anyone (including admins) from pushing a delete ref for the protected branch.

On GitLab: Settings > Repository > Protected branches. Set “Allowed to delete” to “No one” for branches like main and release.

Important

Branch protection rules do not prevent local deletion with git branch -D. They only block the git push --delete that would remove the branch from the remote. Local branches are always under your own control.

Quick Reference
#

GoalCommand
Delete local (safe)git branch -d branch-name
Delete local (force)git branch -D branch-name
Delete remotegit push origin --delete branch-name
List merged into maingit branch --merged main
Prune stale tracking refsgit fetch --prune
Rename current branchgit branch -m new-name
**Trying to delete the branch you are currently on.** Git refuses with "cannot delete branch 'X' checked out at ...". Run `git switch main` first. **Not pruning after teammates delete remote branches.** Your `git branch -r` output shows branches that no longer exist. Run `git fetch --prune` or set `fetch.prune = true` globally to keep tracking refs clean. **Force-deleting unmerged work with `-D`.** Always run `git log feature/branch --oneline` before a force delete to confirm you are not throwing away unique commits. If you already deleted it, check `git reflog` -- the SHA will still be there for 30 days. **Deleting a branch while a PR is open against it.** Closing the PR first (or merging it) is the correct order. Deleting the branch mid-review leaves reviewers in a confusing state on most platforms. **Assuming `git push origin --delete` also prunes local tracking refs.** It does not. You still need `git fetch --prune` to remove the stale `origin/branch-name` ref from your local repo.

If you want to go deeper on any of this, I offer 1:1 coaching sessions for engineers working on AI integration, cloud architecture, and platform engineering. Book a session (50 EUR / 60 min) or reach out at manuel.fedele+website@gmail.com.

Related