git pull --force does not discard your local changes — it only relaxes the check on the fetch side. To make your branch exactly match the remote, fetch first and then hard reset: git fetch origin && git reset --hard origin/main. Untracked files survive that, so git clean -fd is usually the second half of what people actually want.
Almost everyone arrives here after a merge conflict they did not want, on changes they do not care about, on a branch they just want to look like the server. The instinct is to add --force to git pull and move on.
It will not do it. Understanding why takes about a minute, and it is the difference between reliably resetting a branch and occasionally destroying work you meant to keep.
Table of contents
- Why —force does not do what you expect
- What each step actually destroys
- Keeping your work instead of destroying it
- Resetting a branch other than main
- Recovering when you reset something you needed
- How this fits the rest of the stack
- FAQ
Why —force does not do what you expect
git pull is git fetch followed by git merge. The --force flag is passed to the fetch half, where it means: allow updating the local tracking ref even if it is not a fast-forward.
It says nothing about your working directory. Your modified files are still modified, the merge still runs, and the conflict you were trying to avoid still happens.
What people actually want is a different operation: throw away local state and make this branch identical to the remote. That is a reset, and Git keeps it as a separate command deliberately, because it destroys work and should be typed on purpose.
The three-step version, which is the one to memorise:
git fetch origin
git reset --hard origin/main
git clean -fd
Fetch updates your knowledge of the remote. Reset moves your branch and working tree to match it, discarding tracked-file changes. Clean removes untracked files, which reset leaves alone.
What each step actually destroys
Worth being precise, because the three commands have different blast radii.
git fetch origin destroys nothing. It only downloads. You can run it any time, and running it first means the reset targets something real rather than a stale ref.
git reset --hard origin/main discards uncommitted changes to tracked files and moves your branch pointer. Committed work is not gone — it stays in the reflog for around 30 days — but uncommitted edits are genuinely unrecoverable.
git clean -fd deletes untracked files and directories. This is the one that catches people, because it will happily remove a .env file, local uploads, or a config you never committed. Preview it first, always:
git clean -nd # dry run: lists what WOULD be deleted
git clean -fd # actually delete
Note that git clean respects .gitignore by default, so ignored files survive unless you add -x. Adding -x to the command is how people delete node_modules, their local database file and their environment config in one go.
Keeping your work instead of destroying it
Before reaching for a hard reset, it is worth knowing the two commands that make it reversible, because they cost nothing.
Stash. Puts your changes aside and gives you a clean tree:
git stash push -u -m "wip before reset"
git pull
git stash pop # bring them back, or drop them
The -u matters — without it, untracked files are not stashed and will still be there afterwards.
A throwaway branch. Even simpler, and it survives a stash list you forget to read:
git switch -c backup-before-reset
git add -A && git commit -m "snapshot"
git switch main
git reset --hard origin/main
Now the work is a commit on a branch you can return to or delete. This costs about five seconds, and it converts an irreversible operation into a reversible one.
Resetting a branch other than main
The command generalises, and the mistake people make is resetting to the wrong upstream.
git fetch origin
git reset --hard origin/feature-x
If you want to reset to whatever the current branch tracks, without naming it, @{u} refers to the upstream:
git fetch origin
git reset --hard @{u}
That is safer in a script, because it cannot accidentally reset your feature branch to main.
Two related situations worth separating:
- Someone force-pushed and your branch diverged. Reset is correct — your local history references commits that no longer exist upstream.
- You have local commits you want to keep on top of the new remote history. Reset is wrong. Use
git pull --rebaseto replay your commits onto the updated base.
Confusing those two is the main way people lose commits they meant to keep.
Recovering when you reset something you needed
If the work was committed, it is almost certainly still there. The reflog records every position your HEAD has been in:
git reflog
# 8a3f2c1 HEAD@{0}: reset: moving to origin/main
# f91b4e7 HEAD@{1}: commit: the work you just lost
Recover it by branching from the old position:
git switch -c recovered f91b4e7
Reflog entries expire after roughly 30 days for reachable commits and 90 for unreachable ones, so this is a real safety net rather than a theoretical one — but it only covers commits.
Uncommitted changes wiped by reset --hard, and untracked files removed by git clean, are gone. No reflog, no recovery. That asymmetry is the entire argument for the throwaway-branch habit above: committing takes five seconds and moves your work into the category Git can get back.
How this fits the rest of the stack
The distinction underneath all of this is between state that is recorded and state that only exists on one machine. Committed work is recoverable, pushed work is durable, and everything else is one mistyped command from gone. That is worth internalising well beyond Git, because deployments have the same property.
A deploy that came from a commit can be rebuilt and rolled back. A deploy that came from files someone copied onto a server cannot. RunxBuild builds from a connected GitHub repository, keeps per-deploy build and runtime logs, and lets you roll back to a previous deploy — so the running version always has a commit behind it. The RunxBuild hosting calculator shows what a service, database and storage come to together if you are sizing that up.
Useful related references:
- Git Pull All Branches: Why the Command You Want Does Not Exist
- psql Drop Database: DROP DATABASE, FORCE, and the Active-Session Trap
- How to Rename a File in Linux: mv, rename, and git mv
- Services on RunxBuild
FAQ
Does git pull —force overwrite local changes?
No. The flag applies to the fetch half of the operation and only relaxes the fast-forward check on updating refs. Your working directory is untouched and the merge still runs. To overwrite local changes you need git fetch followed by git reset --hard origin/main.
What is the difference between git reset —hard and git clean?
reset --hard discards changes to files Git is tracking and moves your branch pointer. git clean deletes untracked files and directories, which reset does not touch. Making a branch exactly match the remote usually needs both.
Can I recover work after git reset —hard?
If it was committed, yes — find the old position in git reflog and branch from it with git switch -c recovered <hash>. If it was uncommitted, or was an untracked file removed by git clean, it is unrecoverable. That asymmetry is why committing to a throwaway branch first is worth the five seconds.
How do I preview what git clean will delete?
Use git clean -nd for a dry run listing every file and directory it would remove. Note that clean respects .gitignore by default, so adding -x also removes ignored files — which is how people accidentally delete .env files and local databases.
Should I use reset —hard or pull —rebase?
Reset when you want to discard local history entirely and match the remote, typically after someone force-pushed. Rebase when you have local commits worth keeping and want them replayed on top of the updated remote history. Choosing reset in the second case is how commits get lost.