[git] How to git reset --hard a subdirectory?

UPDATE²: With Git 2.23 (August 2019), there's a new command git restore that does this, see the accepted answer.

UPDATE: This will work more intuitively as of Git 1.8.3, see my own answer.

Imagine the following use case: I want to get rid of all changes in a specific subdirectory of my Git working tree, leaving all other subdirectories intact.

What is the proper Git command for this operation?

The script below illustrates the problem. Insert the proper command below the How to make files comment -- the current command will restore the file a/c/ac which is supposed to be excluded by the sparse checkout. Note that I do not want to explicitly restore a/a and a/b, I only "know" a and want to restore everything below. EDIT: And I also don't "know" b, or which other directories reside on the same level as a.

#!/bin/sh

rm -rf repo; git init repo; cd repo
for f in a b; do
  for g in a b c; do
    mkdir -p $f/$g
    touch $f/$g/$f$g
    git add $f/$g
    git commit -m "added $f/$g"
  done
done
git config core.sparsecheckout true
echo a/a > .git/info/sparse-checkout
echo a/b >> .git/info/sparse-checkout
echo b/a >> .git/info/sparse-checkout
git read-tree -m -u HEAD
echo "After read-tree:"
find * -type f

rm a/a/aa
rm a/b/ab
echo >> b/a/ba
echo "After modifying:"
find * -type f
git status

# How to make files a/* reappear without changing b and without recreating a/c?
git checkout -- a

echo "After checkout:"
git status
find * -type f

This question is related to git reset git-reset sparse-checkout

The answer is


A reset will normally change everything, but you can use git stash to pick what you want to keep. As you mentioned, stash doesn't accept a path directly, but it can still be used to keep a specific path with the --keep-index flag. In your example, you would stash the b directory, then reset everything else.

# How to make files a/* reappear without changing b and without recreating a/c?
git add b               #add the directory you want to keep
git stash --keep-index  #stash anything that isn't added
git reset               #unstage the b directory
git stash drop          #clean up the stash (optional)

This gets you to a point where the last part of your script will output this:

After checkout:
# On branch master
# Changes not staged for commit:
#
#   modified:   b/a/ba
#
no changes added to commit (use "git add" and/or "git commit -a")
a/a/aa
a/b/ab
b/a/ba

I believe this was the target result (b remains modified, a/* files are back, a/c is not recreated).

This approach has the added benefit of being very flexible; you can get as fine-grained as you want adding specific files, but not other ones, in a directory.


What about

subdir=thesubdir
for fn in $(find $subdir); do
  git ls-files --error-unmatch $fn 2>/dev/null >/dev/null;
  if [ "$?" = "1" ]; then
    continue;
  fi
  echo "Restoring $fn";
  git show HEAD:$fn > $fn;
done 

According to Git developer Duy Nguyen who kindly implemented the feature and a compatibility switch, the following works as expected as of Git 1.8.3:

git checkout -- a

(where a is the directory you want to hard-reset). The original behavior can be accessed via

git checkout --ignore-skip-worktree-bits -- a

For the case of simply discarding changes, the git checkout -- path/ or git checkout HEAD -- path/ commands suggested by other answers work great. However, when you wish to reset a directory to a revision other than HEAD, that solution has a significant problem: it doesn't remove files which were deleted in the target revision.

So instead, I have begun using the following command:

git diff --cached commit -- subdir | git apply -R --index

This works by finding the diff between the target commit and the index, then applying that diff in reverse to the working directory and index. Basically, this means that it makes the contents of the index match the contents of the revision you specified. The fact that git diff takes a path argument allows you to limit this effect to a specific file or directory.

Since this command fairly long and I plan on using it frequently, I have set up an alias for it which I named reset-checkout:

git config --global alias.reset-checkout '!f() { git diff --cached "$@" | git apply -R --index; }; f'

You can use it like this:

git reset-checkout 451a9a4 -- path/to/directory

Or just:

git reset-checkout 451a9a4

If the size of the subdirectory is not particularly huge, AND you wish to stay away from the CLI, here's a quick solution to manually reset the sub-directory:

  1. Switch to master branch and copy the sub-directory to be reset.
  2. Now switch back to your feature branch and replace the sub-directory with the copy you just created in step 1.
  3. Commit the changes.

Cheers. You just manually reset a sub-directory in your feature branch to be same as that of master branch !!


I'm going to offer a terrible option here, since I have no idea how to do anything with git except add commit and push, here's how I "reverted" a subdirectory:

I started a new repository on my local pc, reverted the whole thing to the commit I wanted to copy code from and then copied those files over to my working directory, add commit push et voila. Don't hate the player, hate Mr Torvalds for being smarter than us all.


Ajedi32's answer is what I was looking for but for some commits I ran into this error:

error: cannot apply binary patch to 'path/to/directory' without full index line

May be because some files of the directory are binary files. Adding '--binary' option to the git diff command fixed it:

git diff --binary --cached commit -- path/to/directory | git apply -R --index

Try changing

git checkout -- a

to

git checkout -- `git ls-files -m -- a`

Since version 1.7.0, Git's ls-files honors the skip-worktree flag.

Running your test script (with some minor tweaks changing git commit... to git commit -q and git status to git status --short) outputs:

Initialized empty Git repository in /home/user/repo/.git/
After read-tree:
a/a/aa
a/b/ab
b/a/ba
After modifying:
b/a/ba
 D a/a/aa
 D a/b/ab
 M b/a/ba
After checkout:
 M b/a/ba
a/a/aa
a/c/ac
a/b/ab
b/a/ba

Running your test script with the proposed checkout change outputs:

Initialized empty Git repository in /home/user/repo/.git/
After read-tree:
a/a/aa
a/b/ab
b/a/ba
After modifying:
b/a/ba
 D a/a/aa
 D a/b/ab
 M b/a/ba
After checkout:
 M b/a/ba
a/a/aa
a/b/ab
b/a/ba

Examples related to git

Does the target directory for a git clone have to match the repo name? Git fatal: protocol 'https' is not supported Git is not working after macOS Update (xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools) git clone: Authentication failed for <URL> destination path already exists and is not an empty directory SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443 GitLab remote: HTTP Basic: Access denied and fatal Authentication How can I switch to another branch in git? VS 2017 Git Local Commit DB.lock error on every commit How to remove an unpushed outgoing commit in Visual Studio?

Examples related to reset

How to reset a form using jQuery with .reset() method Reset all the items in a form How to reset db in Django? I get a command 'reset' not found error How to git reset --hard a subdirectory? How do I revert back to an OpenWrt router configuration? Wipe data/Factory reset through ADB Fastest way to reset every value of std::vector<int> to 0 How do I reset the setInterval timer? How to reset par(mfrow) in R Reset auto increment counter in postgres

Examples related to git-reset

How to undo the last commit in git How can I move HEAD back to a previous location? (Detached head) & Undo commits What is difference between 'git reset --hard HEAD~1' and 'git reset --soft HEAD~1'? Git, How to reset origin/master to a commit? Can I delete a git commit but keep the changes? What is the meaning of git reset --hard origin/master? How to git reset --hard a subdirectory? git undo all uncommitted or unsaved changes Unstaged changes left after git reset --hard How do I use 'git reset --hard HEAD' to revert to a previous commit?

Examples related to sparse-checkout

How to git reset --hard a subdirectory? How to sparsely checkout only one single file from a git repository? Retrieve a single file from a repository How do I clone a subdirectory only of a Git repository? Checkout subdirectories in Git? Can you do a partial checkout with Subversion?