You stage two edited files, then decide that one belongs in a later commit. You want to keep editing it while leaving the other file ready to commit. This Git restore --staged example makes that small but useful operation visible in a disposable repository.
The command for a tracked file in a repository with an existing commit is:
git restore --staged -- notes.txt
It restores the selected index entry from HEAD while leaving the working file alone. The official git-restore manual explains the restoration targets and default source. The double dash separates options from the path. Keep --staged in this operation: plain git restore targets the working tree and can discard edits. The cover is an illustration of staging state; Git does not move the working file into another folder.
Run the lesson in its own repository
Download the complete Git unstage ZIP. It contains demo.py, test_demo.py, a README and expected-output.txt. The Python runner builds its own temporary repository rather than accepting an existing checkout. It makes one fictional initial commit, stages changes to two files, unstages only notes.txt, prints the result and removes the temporary demo.
Use Python 3.10 or later and Git 2.23 or later available as git on PATH. No Python packages, remote repository, credentials or global Git configuration changes are required. The recorded checks used Python 3.12.14 and Git 2.45.0.windows.1 on Windows; other platforms and minimum versions were not separately tested.
git --version
python --version
python demo.py
python -m unittest -v
Extract the ZIP and run those commands from inside git-unstage. Use py -3 on Windows or python3 where that selects your Python installation. If Git cannot be found, install it and confirm that a newly opened terminal can run git --version. If restore is unavailable, check the Git version before trying a different command with a different effect.
Save the complete runner as demo.py
"""Run Git unstage in a disposable repository; Python 3.10+, Git 2.23+."""
from contextlib import contextmanager
from pathlib import Path
import json
import os
import shutil
import subprocess
import tempfile
def git(repo, *args, check=True):
env = {k: v for k, v in os.environ.items() if not k.upper().startswith("GIT_")}
env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull,
GIT_TERMINAL_PROMPT="0")
return subprocess.run([
"git", "-c", "user.name=Demo User", "-c", "user.email=demo@example.invalid",
"-c", "commit.gpgSign=false", "-c", "core.autocrlf=false",
"-c", "core.hooksPath=" + str(repo / "empty-hooks"),
"-C", str(repo), *args
], env=env, capture_output=True, text=True, encoding="utf-8", check=check)
@contextmanager
def fixture():
if shutil.which("git") is None:
raise RuntimeError("Install Git and make its git command available on PATH.")
parent = Path(tempfile.gettempdir()).resolve()
with tempfile.TemporaryDirectory(prefix="toolzify-git-unstage-", dir=parent) as name:
repo = Path(name).resolve()
# Cleanup is restricted to this newly created temporary directory.
if repo.parent != parent or not repo.name.startswith("toolzify-git-unstage-"):
raise RuntimeError("Unexpected temporary repository location.")
(repo / "empty-template").mkdir()
(repo / "empty-hooks").mkdir()
git(repo, "init", "--quiet", "--template=" + str(repo / "empty-template"))
for name in ["notes.txt", "other.txt"]:
(repo / name).write_bytes(b"version one\n")
git(repo, "add", "--", "notes.txt", "other.txt")
git(repo, "commit", "--quiet", "-m", "Create demo fixture")
for name in ["notes.txt", "other.txt"]:
(repo / name).write_bytes(b"version two\n")
git(repo, "add", "--", "notes.txt", "other.txt")
yield repo
def state(repo):
return {
"staged": git(repo, "diff", "--cached", "--name-only").stdout.splitlines(),
"unstaged": git(repo, "diff", "--name-only").stdout.splitlines(),
"notes": (repo / "notes.txt").read_text(encoding="utf-8").strip(),
}
def main():
with fixture() as repo:
before_head = git(repo, "rev-parse", "HEAD").stdout
print("Before:")
print(json.dumps(state(repo), indent=2))
git(repo, "restore", "--staged", "--", "notes.txt")
print("After git restore --staged -- notes.txt:")
print(json.dumps(state(repo), indent=2))
print("HEAD unchanged:", git(repo, "rev-parse", "HEAD").stdout == before_head)
print("Temporary demo removed.")
if __name__ == "__main__":
main()
The runner gives each child Git process an isolated configuration and a fictional per-command identity. It ignores inherited GIT_* settings, avoids user hooks and signing, writes fixed LF bytes, and operates only inside the newly created temporary directory. That keeps the lesson independent of an unrelated checkout or account configuration. The temporary directory's resolved location is checked against its intended parent before cleanup.
Compare the expected output
Before:
{
"staged": [
"notes.txt",
"other.txt"
],
"unstaged": [],
"notes": "version two"
}
After git restore --staged -- notes.txt:
{
"staged": [
"other.txt"
],
"unstaged": [
"notes.txt"
],
"notes": "version two"
}
HEAD unchanged: True
Temporary demo removed.
Before the command, both filenames appear in staged and neither appears in unstaged. Afterward, other.txt remains staged while notes.txt appears in unstaged. The notes value stays version two, and the HEAD comparison is true. Those observations answer three separate questions: which content is selected for the next commit, which content remains on disk, and whether the current commit changed.
The script prints compact filename lists to make the result predictable. In an ordinary repository, inspect the two differences directly:
git diff --cached -- notes.txt
git diff -- notes.txt
git status --short
The git-diff reference describes comparing the index to a commit with --cached and comparing working files to the index without it. The git-status reference explains its short-format columns. Read the staging and working-file states separately; a filename can have changes in both.
Keep the command targeted
The fixture deliberately leaves other.txt staged. This is useful when splitting work into focused commits: removing one path from the proposed snapshot should not silently change the selection for every other path. Check the remaining staged diff before making the next commit, especially if a previous add selected more files than you intended.
To include the current notes file again, stage it explicitly:
git add -- notes.txt
That selects the file's current content. It does not recover a separately staged intermediate version you may have replaced. In the partial-staging test, the index initially contains version two while the working file has version three. Unstaging leaves version three on disk and returns the index entry to version one from HEAD. Keep that distinction in mind before treating the staging area as a backup of an older edit.
Check new files and mistakes as well
The tests include a newly staged file that has no entry in the initial commit. After unstaging it, the file still contains its bytes and Git reports it as untracked. That result is different from the modified tracked notes file. A missing path produces a nonzero Git result without changing the fixture's staged or unstaged lists.
All nine tests passed: initial staging, working-byte preservation, index restoration, unchanged HEAD, the other file remaining staged, restaging, a new file becoming untracked, preservation of a newer working edit and missing-path failure. The ZIP was extracted and executed separately to verify the delivered source and expected output, rather than checking only the original working folder.
Use the result for the right problem
This example changes the proposed next snapshot. It does not undo an existing commit, resolve a merge conflict, alter a remote repository or push anything. The fixture already has an initial commit; a branch without one is outside the lesson. When troubleshooting, first identify whether the unwanted change is staged, only in the working file, or already committed, then choose an operation for that state.
For practice, change the fictional text in the runner and predict the before/after lists before executing it. Keep the working-byte and unchanged-HEAD checks while adapting the exercise. They make it easier to notice when a command that looks similar has a different target.
Comments (0)
Join the discussion