Discard uncommitted changes in a git repo
Throw away local edits and reset the working tree to the last commit — the "I messed up, start over" gesture. Three subtly-different idioms (`git restore`, `git checkout --`, `git reset --hard`) cover three different scopes; picking the wrong one either keeps junk or nukes work you wanted to keep.
How to discard uncommitted changes in a git repo in each shell
git restore .`git restore .` (git 2.23+, Aug 2019) drops UNSTAGED edits in the current directory. To also unstage, add `--staged`: `git restore --staged .`. To do both in one go: `git restore --staged --worktree .`. Pre-2.23 git: `git checkout -- .` (same as restore --worktree). The `.` scopes the operation to the current dir + below — to reset the whole repo regardless of CWD, use `git restore :/` (root path) or `cd $(git rev-parse --show-toplevel)` first.
git restore .git restore .Same git binary; fish has no shell-side wrinkle here. `git restore .` works identically. For the older `git checkout -- .` form, fish does NOT need to escape the `--` — bash/zsh sometimes parse `--` as an option terminator at shell level (rare; safe in modern git).
git restore .Same git binary. To restore the whole repo regardless of CWD: `git restore :/` (pwsh forwards `:/` literally). After running, check `$LASTEXITCODE` (NOT `$?` — `$?` is a generic bool indicating "the host invoked git successfully", not git's exit code). Catch reflog for recovery: `git reflog --date=iso HEAD@{1.hour.ago}` works the same on pwsh as on bash.
git restore .Same git binary. cmd has no `--` quoting quirk. The dot `.` is the path; if your shell-globbing is on (PowerShell-style), git still receives the literal `.` because it's a single argument. For the destructive nuke-everything form `git clean -fd`, prefix with `-n` first (`git clean -fdn`) to DRY-RUN — cmd users especially benefit because there's no `mv .git/index ~/.Trash` recovery story on Windows the way macOS users sometimes assume.
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- **Three operations, three meanings — pick the right one.** `git restore .` discards **unstaged** edits (working tree → match the INDEX); files you `git add`-ed are preserved. `git restore --staged .` does the inverse: unstages everything (INDEX → match HEAD), preserving your working-tree edits. `git restore --staged --worktree .` (or the old `git checkout HEAD -- .`) does BOTH — both staged AND unstaged changes vanish, equivalent to `git reset --hard HEAD` for tracked files but DOES NOT touch untracked files. `git reset --hard HEAD` is the same as `restore --staged --worktree :/`. None of these touch untracked files (new files git doesn't know about) — to also delete those, `git clean -fd`. The full "burn it down" combo: `git reset --hard HEAD && git clean -fdx` (the `-x` also removes ignored files like `node_modules/`).
- **`git restore` vs `git checkout --` — same operation, different ergonomics.** `git checkout` was historically overloaded (switch branches AND restore files AND create branches AND…); git 2.23 split it into `git switch` (branch operations) + `git restore` (file operations). Both still exist; `git checkout -- .` continues to work and means the same as `git restore .`. Use `git restore` in new scripts (clearer intent, harder to misuse — accidentally switching to a branch named `.` is impossible). The `--` separator in `git checkout -- .` prevents git from interpreting `.` as a branch name in the rare collision case.
- **Recovery from `git reset --hard`.** Hard reset is NOT permanent — git's reflog records every HEAD movement for 90 days by default. `git reflog` lists recent HEAD positions: `HEAD@{0}` (current), `HEAD@{1}` (before the reset), and so on. Recover with `git reset --hard HEAD@{1}` or `git checkout HEAD@{1} -- file.txt` for a single file. CAVEAT: reflog only tracks COMMITTED state — work that was never `git add`-ed AND never committed is unrecoverable via git. (Editor undo or filesystem snapshots are your only hope.) `git fsck --lost-found --no-reflog` finds dangling commits the reflog already pruned, written to `.git/lost-found/commit/`.
- **Untracked-file safety: `git clean -fd` deletes without confirmation.** ALWAYS dry-run first: `git clean -fdn` (`-n` = dry-run, no `--dry-run` long form). Untracked files are not in git's history — `git clean -fd` is THE common cause of lost work in this command family. The `-d` flag is "also remove untracked directories" (without `-d`, only loose files in the working dir, not whole new directories). `-x` is "also remove .gitignore'd files" — dangerous because it nukes `node_modules`, `__pycache__`, `.venv`, build artifacts. Use `-X` (capital) to remove ONLY ignored files (keep new untracked source) — useful for "blow away build cache, keep my WIP".
- **Single-file scope vs directory scope.** `git restore path/to/file` restores ONE file. `git restore .` restores the CWD subtree (everything under your current directory). `git restore :/` restores the entire repo regardless of CWD (the `:/` is git's "from repo root" path syntax). `git restore -- file` and `git restore file` are equivalent; the `--` separator is only needed when `file` could be misread as a flag (`git restore -- --weird-filename`). PowerShell users: paths with backslashes need forward-slash conversion for git — `git restore src/app.js`, NOT `git restore src\app.js` (the latter works on Windows-git but breaks the moment the script runs on Linux CI).
Related commands
Related tasks
- Show the current git branch— Print the name of the branch HEAD points at — used in shell prompts, CI build labels, and "deploy what's on the branch" scripts.
- Count files changed since a commit— Get the number of distinct files that differ between a reference commit (a tag, SHA, or `origin/main`) and the working tree — for CI gates and PR-size summaries.
- List modified files in a git repo— Print the paths of files with uncommitted changes (staged or unstaged) — for pre-commit hooks, CI lint-only-changed gates, and "what am I about to commit".