Safely Rebuild Git's Index with git read-tree
You will finish with a controlled way to load a commit into Git's index, merge tree entries, inspect the result, and empty a temporary index without changing the files you are editing. The examples use Git 2.43.0 from Debian package git-man 1:2.43.0-1ubuntu7.3, which is the version installed on this machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Check the installed command
- 2. Inspect a tree in a separate index
- 3. Load a commit into the normal index without touching files
- 4. Preview a reset before changing the index
- 5. Fast-forward the index from two trees
- 6. Update files only when you mean to
- 7. Understand three-way merge entries
- 8. Empty an index only in a controlled workflow
Allow about 20 minutes. You need Git, a test repository, and a working understanding of commits, the index, and the working tree. git read-tree is a plumbing command: it changes the index directly and, only when you request it, updates files in the working tree. It is not a general replacement for git switch or git merge.
Safety boundary
The commands that write the normal index can affect later commits and staged changes. Start in a disposable clone or use a separate index with GIT_INDEX_FILE. Do not use --reset or --empty on a valuable index until you have saved the state you need.
1. Check the installed command
Confirm the binary and version first. This is read-only and does not need elevated privileges:
$ command -v git
/usr/bin/git
$ git --version
git version 2.43.0
$ git read-tree -h
The basic form is git read-tree TREE. A tree-ish can be a commit, a tag that resolves to a commit, or a tree object. Git reads the tree contents into the index, but does not update working files unless the operation includes -u.
Checkpoint: if the version or help output is not what you expect, stop and read the manual installed with that Git package before copying a command from another host.
2. Inspect a tree in a separate index
Use a temporary index when your aim is to inspect a commit rather than stage changes in the repository. The file must be in a directory you can write to:
$ scratch_index="$PWD/.git/read-tree-check.index"
$ GIT_INDEX_FILE="$scratch_index" git read-tree HEAD
$ GIT_INDEX_FILE="$scratch_index" git ls-files
README.md
src/main.c
The paths will differ in your repository. The second command reads the temporary index, so it proves that the tree was loaded without relying on the repository's usual index. The command does not write the contents of those paths into the working directory.
Do not point GIT_INDEX_FILE at .git/index by accident when experimenting. Check it before a state-changing command:
$ printf 'index: %s\n' "${GIT_INDEX_FILE:-.git/index}"
index: .git/read-tree-check.index
When you have finished, remove only the disposable index you created. This is the one destructive cleanup in the example, and it does not remove tracked files:
$ rm -- "$scratch_index"
If you need to keep the inspection state, leave the file in place and record what it represents. An index file without its matching repository and object database is not a portable report.
3. Load a commit into the normal index without touching files
In a repository whose working tree matches the commit you want to inspect, a single-tree read loads the index and leaves files alone:
$ git read-tree HEAD
$ git diff --cached --quiet; printf 'cached diff status: %s\n' "$?"
cached diff status: 0
A zero status from git diff --cached --quiet means the index has no staged difference from HEAD. It does not prove that the working tree is clean. Check both kinds of state when it matters:
$ git status --short
If the command refuses because the index or working tree does not match the tree's expected state, do not add -f or use a reset blindly. Save or commit work first, then retry from a known state.
4. Preview a reset before changing the index
--dry-run checks whether the operation would fail without updating the index or working files. It is useful before a reset or merge:
$ git read-tree --dry-run --reset origin/main
$ printf 'dry-run status: %s\n' "$?"
dry-run status: 0
Replace origin/main with a ref that exists locally. A zero status says that Git accepted the operation under the current conditions. It does not perform the operation, so verify the index again only after running the non-dry command.
--reset is more forceful than a plain read. It discards unmerged index entries instead of refusing, and with -u it does not abort merely because an update could lose working-tree changes or untracked files. That combination can destroy local work. Treat it as a recovery operation, make a backup or use a disposable clone, and do not run it with elevated privileges.
5. Fast-forward the index from two trees
Two tree arguments express a fast-forward-style merge. The first is the current base and the second is the target. Git carries local index and working-tree changes forward only where its rules say they are safe:
$ base=$(git rev-parse HEAD)
$ target=$(git rev-parse origin/main)
$ git read-tree -m "$base" "$target"
$ git diff --cached --name-status "$target"
The final command lists staged differences between the resulting index and the target tree. No output means the index matches the target for the paths Git compared. A non-zero command or an error about an entry not being up to date means that local work needs attention; it is a protection against losing changes, not a request to retry with --reset.
Without -u, this operation updates the index only. That separation is useful when another command will update files later, but it can also surprise you: the index and working tree may temporarily describe different states.
6. Update files only when you mean to
Add -u to ask a successful merge to update the working tree as well:
$ git read-tree -m -u "$base" "$target"
$ git status --short
There may be no status output if the index and files now agree. Existing local edits that would be overwritten should make the command fail rather than disappear. Still, treat -u as a destructive boundary: review git status --short, save important work, and use --dry-run first.
There is no undo flag for a successful index or working-tree update. Recovery is ordinary Git recovery: restore a saved copy, use git restore from a known commit where appropriate, or recover committed content from the object database. Uncommitted edits that were overwritten cannot be reconstructed reliably by git read-tree.
7. Understand three-way merge entries
With three trees and -m, Git uses the arguments in significant order: common ancestor, current branch, then the other branch. Differences that Git cannot resolve trivially remain in the index at stages 1, 2, and 3:
$ ancestor=$(git merge-base HEAD topic)
$ ours=$(git rev-parse HEAD)
$ theirs=$(git rev-parse topic)
$ git read-tree -m "$ancestor" "$ours" "$theirs"
$ git ls-files -u
100644 1111111111111111111111111111111111111111 1 path/to/file
100644 2222222222222222222222222222222222222222 2 path/to/file
100644 3333333333333333333333333333333333333333 3 path/to/file
The object IDs above are illustrative placeholders, not output to copy. Your output contains real IDs. Stage 1 is the ancestor, stage 2 is the current branch, and stage 3 is the other tree. An index containing these unmerged entries cannot be written as a normal tree until a higher-level merge workflow resolves each path.
git read-tree handles only trivial cases itself. It does not edit conflict markers into files or choose a project-specific merge policy. If you need a normal user-facing merge, use git merge; if you are implementing plumbing, inspect the stages and resolve them deliberately before using git write-tree.
8. Empty an index only in a controlled workflow
--empty replaces the selected index contents with an empty index. It does not delete tracked files from the working tree by itself:
$ isolated_index="$PWD/.git/empty-test.index"
$ GIT_INDEX_FILE="$isolated_index" git read-tree HEAD
$ GIT_INDEX_FILE="$isolated_index" git read-tree --empty
$ test -z "$(GIT_INDEX_FILE="$isolated_index" git ls-files)" && echo 'index is empty'
index is empty
This pattern is suitable for scripts that construct an index before writing a tree. It is also a trap if run against the normal index: later commands may report every tracked file as untracked or absent from the index. Keep the operation isolated unless your workflow explicitly requires an empty normal index.
Done means
- You checked the installed Git 2.43.0 command and its local help.
- You used a separate
GIT_INDEX_FILEfor experiments and checked its path. - You know that a normal read changes the index, while
-ucan change working files too. - You previewed risky operations with
--dry-runand did not use--resetas a shortcut around saved work. - You can read stages 1, 2, and 3 from a three-way index and know when a porcelain merge is the safer tool.
- You have a recovery path for any command that changes the normal index or working tree.