Last time, we looked at ways to return a repo to a state that matched an earlier commit. But what if you don’t want to return the entire repo to that state, just a subdirectory?
A simple way to return a file or group of files to an earlier state is to check them out based on an earlier commit:
# Warning: Read caveats below before using git checkout A -- some_file git checkout A -- wildcards* git checkout A -- some_directory git checkout A -- .
This takes file or group of files as it was as of a commit and copies them to the index as well as putting them in your working tree.
The caveat is that it affects only files that were present in commit A. If a file was added some time after commit A, then the git checkout
won’t delete it.¹
Fortunately, you can detect whether this has happened by doing a diff
:
git diff --cached A --name-only -- some_file git diff --cached A --name-only -- wildcards* git diff --cached A --name-only -- some_directory git diff --cached A --name-only -- .
If the output is empty, then everything matches.
If the output is not empty, then something is different, but the output doesn’t tell the whole truth. If any files were renamed, then the output will show the newly-created file that resulted from the rename, but will omit the no-longer-present file. To get the true list of affected files, you need to disable rename detection.
git diff --cached A --name-only --no-renames -- whatever
Even though the git checkout
method has its flaws, they are know flaws that you can detect with some follow-up commands, and git checkout
does have the advantage of using commands that you are already familiar with.
But my preference when trying to do tree surgery is just to do tree surgery directly, rather than trying to find equivalent commands that have the same effect as tree surgery.
git rm --cached -r subdirectory git read-tree --prefix:subdirectory A:subdirectory
The first command removes the subdirectory from the index. This is a preparatory step, because git read-tree
will get mad if you try to graft a tree into a place where a tree already exists.
The second command is the money. It takes a subtree from commit A and grafts into into the index at the specified location.
In the above example, I placed the subtree in the same location as it came from, but you don’t have to do it that way.
git read-tree --prefix:archive/src A:src
The above version takes the src
subtree from commit A and adds it to the index as a subdirectory named archive/src
.
Once the changes are in the index, you can commit them directly with git commit
, or you can git reset
them to unstage the changes, or you can git add
more files to the change (say, to update a README
). If you want to make changes to the files you staged, you can copy them into your worktree with git checkout --
.
Basically, the grafting is all staged, and you can use regular git commands to do whatever it is you want to do next.
¹ That doesn’t stop people from using it for this purpose. I hope they understand where the holes are.
https://git-scm.com/docs/git-restore is new-ish (added in git 2.23) and will restore contents from an arbitrary source, while also removing files that didn’t exist in the source.
https://git-scm.com/docs/git#_reset_restore_and_revert is also a good explanation of when to use restore vs other very similar commands
Hm, you are right, I didn’t really think about deleted or renamed files in my previous comment, as it happened to never have been an issue so far. Thanks for bringing this to my attention.
Off topic comment – but there is an interesting post today from Bruce Dawson over at RamdonASCII https://randomascii.wordpress.com/2020/10/04/windows-timer-resolution-the-great-rule-change/ that might make an interesting NewOldThing topic.