Common Git Errors and How to Fix Them
Git prints an error for almost every situation it cannot resolve on its own: a rejected push, a merge conflict, a detached HEAD, an authentication failure. These messages look intimidating the first time you see them, but Git is usually very explicit about what went wrong, and it often prints a hint: line telling you exactly what to try next. This lesson walks through the errors you will run into most often, explains precisely why Git raises them, and gives you a safe, repeatable way to fix each one without losing work.
Overview: how Git errors work
Every Git command operates on some combination of three things: the working tree (the files you see and edit), the index (also called the staging area, a snapshot of what the next commit will contain), and refs (movable pointers like branches and HEAD that point at commit objects). A commit object records a snapshot: it points to a tree object representing the state of the whole project at that moment, and that tree points to blob objects (file contents) and further trees (subdirectories). A branch such as main is nothing more than a file containing the SHA-1 hash of a commit; HEAD normally points at a branch, not directly at a commit. Almost every error you will see comes from Git refusing to silently overwrite or lose one of these three things.
Broadly, the errors in this lesson fall into a few families:
- Local-state errors, where uncommitted changes in your working tree or index would be overwritten by the operation you asked for.
- History-divergence errors, where your local branch and the remote branch have both moved forward independently, so Git cannot fast-forward one onto the other.
- Conflict errors, where the same lines of the same file were changed differently in two histories being combined.
- Reference-state errors, such as ending up in a "detached HEAD", where
HEADpoints straight at a commit instead of at a branch. - Authentication and network errors, where GitHub rejects your credentials or Git cannot reach the remote at all.
In every case, Git’s design goal is the same: never throw away work without being asked to. Once you recognize which family an error belongs to, the fix becomes predictable.
Syntax: your diagnostic toolkit
Before fixing anything, use these four commands to see exactly what state your repository is in. None of them change anything, so they are always safe to run first.
git status
git log --oneline --graph --decorate -10
git diff
git reflog
git status— shows the branch you’re on, whether it’s ahead/behind the remote, what’s staged, what’s modified, and what’s untracked. It almost always prints a hint about the next command to run.git log --oneline --graph --decorate— shows recent commits as a compact graph with branch and tag labels, so you can see where branches have diverged.git diff— shows unstaged changes (add--stagedto see staged changes instead).git reflog— shows a log of every placeHEADhas pointed, including commits that are no longer on any branch. This is your safety net for "undoing" almost anything, including a badreset --hard.
The table below maps the error text you will actually see to what it means and the typical fix; each is covered in more depth further down.
| Error message (excerpt) | What it means | Typical fix |
|---|---|---|
fatal: not a git repository |
The current directory isn’t inside a Git working tree | cd into the repo, or run git init |
! [rejected] ... (fetch first) |
The remote has commits you don’t have locally; a non-fast-forward push | git fetch then git rebase or git pull --rebase, then push again |
CONFLICT (content): Merge conflict in <file> |
The same lines changed differently in both histories being combined | Edit the file to resolve, then git add and continue |
fatal: refusing to merge unrelated histories |
The two histories share no common ancestor commit | Confirm this is intentional, then add --allow-unrelated-histories |
Your local changes ... would be overwritten |
Uncommitted changes conflict with an incoming checkout, merge, or pull | git stash, run the operation, then git stash pop |
You are in 'detached HEAD' state |
HEAD points directly at a commit instead of a branch |
git switch -c <new-branch> to keep work, or git switch main to leave |
Permission denied (publickey) |
Git can’t authenticate your SSH key with GitHub | Check ssh-agent and that your public key is added to your GitHub account |
Support for password authentication was removed |
You tried to push over HTTPS using a plain password | Use a Personal Access Token or switch to an SSH remote URL |
Examples
Example 1: rejected push (non-fast-forward)
You commit locally and try to push, but a teammate already pushed to main since you last fetched.
git push origin main
Output:
To https://github.com/alice/recipe-app.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://github.com/alice/recipe-app.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git refuses the push because it can only move the remote’s main pointer forward (a "fast-forward"); accepting your push would silently discard your teammate’s commit. Bring your branch up to date first, then push again:
git fetch origin
git rebase origin/main
git push origin main
git fetch downloads the new commits without touching your working tree. git rebase origin/main replays your local commits on top of the updated remote branch, producing a linear history. The push now succeeds because your branch is a fast-forward of the remote’s. (You could use git merge origin/main instead of rebase; that keeps a merge commit rather than rewriting your commits — either is fine on a branch only you use.)
Example 2: a merge conflict
You merge main into your feature branch, and both branches changed the same function.
git switch feature/login-page
git merge main
Output:
Auto-merging src/login.js
CONFLICT (content): Merge conflict in src/login.js
Automatic merge failed; fix conflicts and then commit the result.
Git merges every file it can automatically, but it stops on src/login.js and writes conflict markers directly into the file so you can decide the outcome:
<<<<<<< HEAD
export function login(username, password) {
return authenticate(username, password, { rememberMe: true });
}
=======
export function login(user, pass) {
return authenticate(user, pass);
}
>>>>>>> main
Everything between <<<<<<< HEAD and ======= is your branch’s version; everything between ======= and >>>>>>> main is the incoming version. Edit the file down to the single correct version, remove all three marker lines, then stage and commit:
git add src/login.js
git commit -m "fix: resolve merge conflict between feature/login-page and main"
Committing here doesn’t create a normal commit message editor by accident — Git pre-fills a merge commit message for you; you can accept it or edit it, as shown above.
Example 3: detached HEAD
You check out a specific commit by its hash instead of a branch name, to inspect old code.
git log --oneline -5
git checkout a1b2c3d
Output:
Note: switching to 'a1b2c3d'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at a1b2c3d Add password reset endpoint
HEAD now points straight at commit a1b2c3d instead of at a branch. This is completely safe for looking around, but if you commit new work here, those commits belong to no branch — if you switch away without saving them somewhere, they become unreachable from any ref (though git reflog can still find them for a while). Pick one of two exits depending on what you want:
# Option A: keep any new work by giving it a branch name
git switch -c hotfix/inspect-old-build
# Option B: you were only looking, just go back
git switch main
How it works step by step
Rejected push: Git compares the SHA-1 of the commit your local main thinks the remote main is at against the commit the remote actually reports. If they differ and your commit isn’t a descendant of the remote’s, the push is rejected before any data is sent, because accepting it would move the remote pointer to a commit that doesn’t contain the remote’s latest work.
Merge conflict: a three-way merge compares the common ancestor commit, your branch tip, and the other branch tip, line by line, for every file. Where only one side changed a line, Git takes that side automatically. Where both sides changed the same line differently, Git cannot choose, so it writes both versions into the working tree file between conflict markers and leaves the index in a special "unmerged" state until you resolve it with git add.
Detached HEAD: normally the file .git/HEAD contains a reference like ref: refs/heads/main, one level of indirection. Checking out a raw commit hash writes that hash directly into .git/HEAD instead, removing the indirection. Any new commit still gets created normally as an object in .git/objects, but no branch pointer moves to include it, which is what makes it easy to lose track of.
Common Mistakes
1. Force-pushing with a bare --force over a shared branch.
git push --force origin main
This is wrong because it overwrites whatever is on the remote unconditionally, even commits a teammate pushed seconds ago that you haven’t fetched yet — those commits are simply gone from the branch. Use --force-with-lease instead, which fails safely if the remote has moved since you last fetched it:
git push --force-with-lease origin main
2. Committing to the wrong branch. You meant to be on a feature branch but were still on main when you ran git commit.
git switch main
git log --oneline -1
git switch -c fix/typo-in-readme
git switch main
git reset --hard HEAD~1
git switch fix/typo-in-readme
This is wrong to leave as-is because it pollutes main‘s history and, if pushed, affects everyone tracking that branch. The fix creates a new branch at the current commit (capturing your mistaken commit), then moves main back one commit with git reset --hard HEAD~1. Only run reset --hard on commits that have not been pushed yet — it discards the commit and any uncommitted local changes on the branch you run it on.
3. Forgetting to stage a file before committing.
git commit -m "feat: add search bar to navbar"
Output:
On branch main
Changes not staged for commit:
(use "git add ..." to update what will be committed)
modified: src/components/Navbar.jsx
no changes added to commit (use "git add" and/or "git commit -a")
Git only commits what is in the index, not everything in the working tree — this is by design, so you can build a commit deliberately. Stage the file and commit again:
git add src/components/Navbar.jsx
git commit -m "feat: add search bar to navbar"
Best Practices
- Read the last few lines of an error first — Git’s
hint:lines usually name the exact command to run next. - Run
git statusbefore and after any operation you’re unsure about; it never modifies anything. - Prefer
git push --force-with-leaseover bare--forceon any branch someone else might also push to. - Never rebase or force-push a branch that others have already pulled and built work on top of — that’s Git’s golden rule for history rewriting.
- Commit or stash work in progress before switching branches or pulling, so you never have to fight a "would be overwritten" error.
- Keep
git reflogin mind as a safety net — a "lost" commit after a bad reset is almost always still recoverable for a while. - Use a Personal Access Token or SSH key for GitHub, never a plaintext password, since GitHub no longer accepts password auth over HTTPS.
- Write commit messages using a consistent style, such as Conventional Commits (
fix: ...,feat: ...), so history stays easy to scan when you’re troubleshooting.
Practice Exercises
- Create a local repo, make a commit, then simulate a teammate’s push by cloning it to a second folder, committing there, and pushing. Push from the original folder and reproduce the rejected-push error; resolve it with
fetchandrebase. - On a single repo, create two branches from the same commit, edit the same line of the same file differently on each, then merge one into the other. Resolve the resulting conflict by hand and commit the result.
- Run
git checkoutagainst an old commit hash to enter detached HEAD, make a small commit there, then recover it onto a proper branch usinggit switch -c. Confirm withgit logthat the commit is now reachable from a branch.
Summary
- Most Git errors exist to stop an operation from silently discarding work in your working tree, index, or on the remote.
- A rejected push means the remote has moved since you last fetched — fetch and rebase or merge before pushing again.
- A merge conflict means the same lines changed on both sides — edit out the conflict markers, then
git addand commit. - Detached HEAD means
HEADpoints at a commit instead of a branch — create a branch withgit switch -cto keep any new work. git status,git log --graph,git diff, andgit reflogare your primary, side-effect-free diagnostic tools.- Prefer
--force-with-leaseover--force, and never rewrite the history of a branch others already rely on.
