mulllhausen
mulllhausen

Reputation: 4435

git checkout <commit> . is not removing files that were added after the commit

Using a public repo, I want to get my master branch back to a certain commit from the past. I have reviewed the options and the best thing for me looks to be a simple checkout to the desired commit, then commit to the master branch. However when I do the checkout it does not remove some files that have been added into master after the specified commit hash.

So for example, if I want to get back to commit aaa1:

$ cd working-copy-top-dir
$ git checkout master
$ git checkout -- .
$ git clean -fd
$ git checkout aaa1 .
$ git clean -fd

But at this point some files added after aaa1 are still in the working copy. What is the checkout command to get the working copy data back how it was at aaa1?

$ git --version
git version 2.7.2.windows.1

Upvotes: 19

Views: 22176

Answers (6)

git checkout --no-overlay (git 2.22.0, June 2019)

With this this option running:

git checkout --no-overlay <commit> <directory>

removes all files that were added under <directory> after <commit>. I wish that were the default behavior, but c'est la vie.

The option was added by Thomas Gummerer at 091e04bc8cbb0c89c8112c4784f02a44decc257e which went into git v2.22.0.

Test:

#!/usr/bin/env bash
set -eu

rm -rf tmp
mkdir tmp
cd tmp
git init

mkdir a
touch a/a
git add .
git commit -m a

mkdir b
touch b/b
git add .
git commit -m b

git checkout HEAD~ .
echo 'overlay'
ls -l . a b

git checkout --no-overlay HEAD~ .
echo 'no overlay'
ls -l . a b

Outcome:

git checkout HEAD~ .
echo 'overlay'
ls -l . a b
echo

git checkout --no-overlay HEAD~ .
echo 'no overlay'
ls -l . a b
echo

Output:

Initialized empty Git repository in /home/ciro/test/git/tmp/.git/
[master (root-commit) 216d22e] a
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 a/a
[master a36e67b] b
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 b/b
overlay
.:
total 2
drwxrwxr-x 2 ciro ciro 3 Jan 17 08:40 a
drwxrwxr-x 2 ciro ciro 3 Jan 17 08:40 b

a:
total 1
-rw-rw-r-- 1 ciro ciro 0 Jan 17 08:40 a

b:
total 1
-rw-rw-r-- 1 ciro ciro 0 Jan 17 08:40 b

no overlay
ls: cannot access 'b': No such file or directory
.:
total 1
drwxrwxr-x 2 ciro ciro 3 Jan 17 08:40 a

a:
total 1
-rw-rw-r-- 1 ciro ciro 0 Jan 17 08:40 a

So we see that b/b only gets removed with --no-overlay. The directory b then also gets removed as usual in git commands because it would have become empty as a result of the git command.

Tested on Ubuntu 22.10, Git 2.37.2.

Upvotes: 1

Arseniy Alekseyev
Arseniy Alekseyev

Reputation: 73

I found that this command updates both the index and the working copy to the commit that I want, given by variable treeish in this case:

# use with care: destroys any uncommitted changes
git read-tree "$treeish" --reset -u

To update the working copy without updating the index, you could perhaps do something like this:

index_bak=$(git write-tree)
git read-tree "$treeish" --reset -u
git read-tree "$index_bak" --reset

Upvotes: 0

ElpieKay
ElpieKay

Reputation: 30956

git stash may be the quickest way to clean up the working tree. and then git checkout -b $newbranch $commit-sha1-you-want to create a branch you are going to work with. after all your work's done, git stash pop to restore the working tree.

Upvotes: -1

torek
torek

Reputation: 490038

TL;DR: remove everything first

When you used git checkout aaa1 ., you told Git to translate aaa1 to a commit, find that commit (more precisely, its tree), and copy every file in that commit to your index / staging area and work-tree.

Let's say, just for the sake of argument, that you start with master containing two files, README and hello:

$ git checkout master
[output snipped]
$ ls
README   hello
$ cat README
Yay, you read me!
$ cat hello
world
$ 

Let's say further that commit aaa1 exists and has two files in it, README and addendum. Its README says Thank you for reading. Let's do that checkout:

$ git checkout aaa1 -- .
[output snipped]
$ ls
README    addendum  hello

(I added the --: it's not actually required here, but it's good practice.) The contents of README are the updated README. The file addendum has also been extracted. The file hello is not removed and remains unchanged from the version found in master. The updated README and hello are staged:

$ git status --short
M  README
A  addendum

but hello is not removed:

$ git ls-files --stage
100644 ac6f2cf1acbe1b6f11c7be2288fbae72b982823c 0   README
100644 7ddf1d71e0209a8512fe4862b4689d6ff542bf99 0   addendum
100644 cc628ccd10742baea8241c5924df992b5c019f71 0   hello

Using git clean, even with -x, will have no effect: nothing needs cleaning; there are no unstaged files (hello is staged, it's just not modified).


You specifically wanted to get the work-tree to match commit aaa1, byte for byte. To do that, you must find files that are in the index now, but were not in aaa1, and remove them.

There is, however, an easier way: just remove everything. Then, use your git checkout aaa1 -- . to extract everything from aaa1. This will fill in the index and work-tree from aaa1: any files that need to be restored to the way they were before removing, are restored (to the way they were in aaa1 which is the same as the way they are in HEAD). Any files that need to be changed to match the way they were in aaa1, are restored (to the way they were in aaa1 which is different).

$ git rm -rf .
rm 'README'
rm 'addendum'
rm 'hello'
$ git checkout aaa1 -- .
$ git ls-files --stage
100644 ac6f2cf1acbe1b6f11c7be2288fbae72b982823c 0   README
100644 7ddf1d71e0209a8512fe4862b4689d6ff542bf99 0   addendum
$ git status --short
M  README
A  addendum
D  hello

You can now commit and you will have a new commit on master that, regardless of what was there before, has exactly the same tree as aaa1.

(Whether this is a good idea is another thing entirely, but it will get you the desired state.)

Upvotes: 15

mulllhausen
mulllhausen

Reputation: 4435

Git checkout will not remove files added since a previous commit. To do this I would need git revert.

However I find git checkout thehash . a lot easier to use, and its not so hard to see which files have been added since that hash:

git diff --name-status HEAD thehash

Upvotes: 1

pRaNaY
pRaNaY

Reputation: 25320

Do you want to roll back your repo to that state? Or you just want your local repo to look like that?

See https://git-scm.com/docs/git-reset for git reset.

CASE 1: if you do

git reset --hard [commit hash]

It will make your local code and local history be just like it was at that commit. But then if you wanted to push this to someone else who has the new history, it would fail.

CASE 2: if you do

git reset --soft [commit hash]

It will make your local files changed to be like they were then, but leave your history etc. the same.

I found answer here. Also you can see related answer here.

Upvotes: 2

Related Questions