Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to git reset --hard a subdirectory?

People also ask

How do I Unstage a folder?

In order to unstage all files and directories, execute “git reset” and they will be removed from the staging area back to your working directory.

How do I checkout a specific directory in git?

To checkout everything from your HEAD (not index) to a specific out directory: git --work-tree=/path/to/outputdir checkout HEAD -- .


With Git 2.23 (August 2019), you have the new command git restore (also presented here)

git restore --source=HEAD --staged --worktree -- aDirectory
# or, shorter
git restore -s@ -SW  -- aDirectory

That would replace both the index and working tree with HEAD content, like an reset --hard would, but for a specific path.


Original answer (2013)

Note (as commented by Dan Fabulich) that:

  • git checkout -- <path> doesn't do a hard reset: it replaces the working tree contents with the staged contents.
  • git checkout HEAD -- <path> does a hard reset for a path, replacing both the index and the working tree with the version from the HEAD commit.

As answered by Ajedi32, both checkout forms don't remove files which were deleted in the target revision.
If you have extra files in the working tree which don't exist in HEAD, a git checkout HEAD -- <path> won't remove them.

Note: With git checkout --overlay HEAD -- <path> (Git 2.22, Q1 2019), files that appear in the index and working tree, but not in <tree-ish> are removed, to make them match <tree-ish> exactly.

But that checkout can respect a git update-index --skip-worktree (for those directories you want to ignore), as mentioned in "Why do excluded files keep reappearing in my git sparse checkout?".


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

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

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

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.


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.