How to git reset --hard a subdirectory
Asked Answered
V

10

299

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
Vale answered 14/3, 2013 at 8:40 Comment(10)
what about a git stash && git stash drop ?Tactful
what about git checkout -- /path/to/subdir/?Clubman
@CharlesB: git stash doesn't accept a path argument...Vale
@iberbeu: Nope. Will also add files excluded by sparse checkout.Vale
Re: bounty. This isn't the place for answers from credible / official sources or for responses from git developers. To log a bug about sparse checkout you should use the git mailing list [email protected] .Israelisraeli
It doesn't look like the sparse checkout feature is something intended to be really used... just don't use it and set up a separate git repo for every project.Hamblin
@CharlesBailey: Then why is there a radio button reading "Looking for an answer drawing from credible and/or official sources." in the bounty dialog? I didn't type that myself! Also try googling "git reset subdirectory" (without the quotes) and see what's on the first 3 positions. For sure a message to a kernel.org mailing list will be more difficult to find. -- Also, to me it's not clear yet if this behavior is a bug or a feature.Vale
@user1050755: This won't allow me using bisect and friends over multiple projects simultaneously. -- I think sparse checkout is a sweet feature, bugs or annoyances can be fixed.Vale
I was just saying that if you want a response from the Git developers then you should ask in the appropriate place; it seems a bit pointless to hope that one will come here or that someone will relay the message on when the mailing list exists as a direct way for you to communicate with Git developers.Israelisraeli
@CharlesBailey: Right. I will post a link to the mailing list (thanks for the address btw), let's see if there will be more feedback.Vale
Y
333

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?".

Yon answered 14/3, 2013 at 8:51 Comment(11)
Please clarify. After git checkout HEAD -- ., files excluded by sparse checkout reappear. What is git update-index --skip-worktree supposed to do?Vale
@Vale skip-worktree or assume-unchanged are the two way of trying to make an entry in the index "invisible" for git: fallengamer.livejournal.com/93321.html, https://mcmap.net/q/11287/-git-difference-between-39-assume-unchanged-39-and-39-skip-worktree-39/6309 and https://mcmap.net/q/11356/-git-assume-unchanged-vs-skip-worktree-ignoring-a-symbolic-linkYon
@Vale those links are just pointers for you to try and see if a checkout would still restore those entries, once they have been marked as 'skipped-worktree'.Yon
Sorry, but that's too complicated for the task at hand. I want an inclusive reset, not an exclusive one. Is there really no nice way to do this in Git?Vale
@Vale no: best to do the git checkout HEAD -- <path>, and then delete the directories that were restored (but which are still declared in the sparse checkout).Yon
git checkout HEAD -- <path> doesn't seem to behave the same as a hard reset. A hard reset removes files in the path that are no longer in the given revision. In my experience (git 1.8.3.3), git checkout HEAD -- <path> does not.Fibroid
@Yon Would be nice to see a solution that also will remove files from the revision. Right now I have an alias to simulate the behavior of reset --hard with a path: reset-path = "!f() { : git checkout ; rm -rf \"$2\" && git checkout \"$1\" -- \"$2\" ; }; f". However, this will also remove untracked files when it shouldn't, but it's a reasonable tradeoff for the convenience it provides at the moment.Dunlop
That's a pretty god awful looking command for something seemingly simple, but it worked.Othilie
This does not remove new files.Shellyshelman
@Shellyshelman no, I suppose a git clean would be needed to get rid of those.Yon
@vsync Agreed. That is also an answer of mine on git restore. I have added a link to said answer to this current answer.Yon
V
148

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
Vale answered 16/5, 2013 at 14:0 Comment(5)
Thanks for your effort following up with the Git development team, resulting in this change in Git.Debor
And note that "a" in this case means the directory you want to revert, so if you're in the directory you want to revert, the command should be git checkout -- . where . means the current directory.Pullen
One comment from my side is that you must unstage the folder first with git reset -- a (where a is the directory you want to reset)Robalo
Isn't that true also if you want to git reset --hard the whole repo?Vale
And if you added any new files to that directory, do a rm -rf a before.Menorah
D
35

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
Debor answered 16/3, 2013 at 22:26 Comment(3)
Sounds good. But shouldn't git checkout respect the "skip-worktree" bit in the first place?Vale
A quick review of checkout.c and tree.c does not reveal that the skip-worktree flag is used.Debor
This is simple enough to be useful in practice, even if I will have to set up a bash alias for this command. Duy Nguyen has replied to my message to the Git mailing list, let's see if a more user-friendly alternative will pop up soon.Vale
F
19

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
Fibroid answered 16/1, 2015 at 16:45 Comment(3)
I saw your comment yesterday and experimented it today. Your alias is helpful. +1Yon
How does this option compare to git checkout --overlay HEAD -- <path> command that @Yon mentions in his answer?Letendre
@EhteshChoudhury Note that git checkout --overlay HEAD -- <path> is not released yet (Git 2.22 will be released in Q2 2019)Yon
W
9

ALL GIT VERSION

For me I will use checkout:

First I remove the folder I want to reset.

Then I will simply use checkout:

git checkout <branch> <path>

The folder will be reset correctly to the branch I want.

Wootten answered 30/6, 2022 at 10:45 Comment(0)
I
6

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 Linus Torvalds for being smarter than us all.

Ireful answered 13/9, 2018 at 23:21 Comment(0)
E
5

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 subdirectory:

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

Cheers. You just manually reset a subdirectory in your feature branch to be same as that of the master branch!

Exterminatory answered 7/7, 2017 at 18:37 Comment(1)
I didn't see any votes for this answer, but sometimes this is the simplest guaranteed route to success.Coulometer
P
4

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.

Pogue answered 16/3, 2013 at 22:26 Comment(3)
That's nice, but I'd have to git add everything except a, right? Sounds difficult in practice.Vale
@Vale Not really. You can git add . then git reset a to add everything except a.Pogue
@Vale Also, it's worth noting that git add doesn't add deleted files. So, if you're only recovering deleted files, git add . will add all the modified files, but not the deleted ones.Pogue
G
1

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
Genseric answered 3/1, 2017 at 14:55 Comment(0)
M
0

Use:

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
Maladjustment answered 19/3, 2020 at 14:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.