Home > Software design >  How to understand `in the way of writing any tracked files` in `git reset --hard`?
How to understand `in the way of writing any tracked files` in `git reset --hard`?

Time:09-17

In git-reset, the hard mode:

--hard
Resets the index and working tree. Any changes to tracked files in the working tree since are discarded. Any untracked files or directories in the way of writing any tracked files are simply deleted.

How to understand Any untracked files or directories in the way of writing any tracked files are simply deleted?

I did a small experiment: Create a dummy file

touch dummy.html
git status -s

History as below:

b3579f4 (HEAD -> main) Revert "Add a crazzzy experiment"
622be1d Add a crazzzy experiment

git reset --hard 622be1d, the dummy file is intact.

➜  my-git-repo git:(main) ✗ git reset --hard 622be1d
HEAD is now at 622be1d Add a crazzzy experiment
➜  my-git-repo git:(main) ✗ git status -s
?? dummy.html

git reset --hard b3579f4, the dummy file is untouched.

➜  my-git-repo git:(main) ✗ git reset --hard b3579f4
HEAD is now at b3579f4 Revert "Add a crazzzy experiment"
➜  my-git-repo git:(main) ✗ git status -s
?? dummy.html

It looks like that --hard mode has no effect on the untracked file. So what's the meaning of in the way of writing any tracked files? How to simulate this kind of situation?

CodePudding user response:

There is a case in which git reset --hard has effects on untracked files.

touch a.txt
git add .
git commit -m'init'

echo hello >> a.txt
git add .
git commit -m'hello'

git rm a.txt
echo world > a.txt
git status -s

The status output is

D  a.txt
?? a.txt

The a.txt in the index is removed and the one in the work tree is untracked.

cat a.txt
world

Reset in the mode of --hard,

git reset --hard
cat a.txt
hello

git status
On branch master
nothing to commit, working tree clean

The untracked a.txt in the work tree is deleted. But we could also say it's been overwritten with the tracked a.txt in HEAD.

CodePudding user response:

Let's start with the definition of a tracked file, which is remarkably simple: A tracked file is a file that is in Git's index. Such a file has a name, e.g., path/to/file.ext. Note that there is no such thing as a directory or folder here, it's just a long name with forward slashes—always forward slashes even if the host OS (Windows) is going to call this path\to\file.ext.

The simple answer, then, lies in observing that Git is going to write path/to/file.ext out to your working tree because path/to/file.ext is in Git's index and git reset --hard therefore needs to write path\to\file.ext (Windows) or path/to/file.ext (other OSes). In order to do this, path must be a directory (AKA folder), and path/to or path\to—whichever will be used—must also be a folder.

So, if there's an existing file named path or path/to, Git will remove this file. And that's the simple answer to the question about what the phrase you quoted means.

But wait, there's more

There's an issue here. Git's index is separate from Git's commits, and git reset with --mixed or --hard is going to fill in Git's index from the commit you pick:

git reset --mixed a123456

tells Git to look up commit a123456 (by hash ID in this case; you could equally say git reset --mixed origin/zorg to use remote-tracking name origin/zorg to find the hash ID first).

Having found commit a123456 (or whatever hash ID you've specified), Git must now:

  • remove, from its index, any files that aren't in a123456;
  • add, to its index, any files that are in a123456.

These "remove" and "add" steps may involve touching the working tree.

If we use git reset --soft, we tell Git don't touch index or working tree at all, so that only the first step, moving the branch whose name is stored in HEAD, occurs. If we use git reset --mixed, we tell Git don't touch the working tree at all, so that only the branch-move and index-resetting occur. But when we use --hard, the working tree must be updated.

The "remove a file from the index" part of this update will also remove the corresponding file from the working tree.

To observe this happening, start by creating a file in the working tree, leaving it untracked as you did:

echo data > some-file

Running git status will show it as untracked (?? some-file in git status -s output, for instance). As you already saw, git reset --hard won't change this.

Now copy the file into Git's index using:

git add some-file

Now git status will show this as a new file to be committed. It's not in the current (HEAD) commit, so the diff from HEAD commit to index / staging-area shows some-file as new. It is in both Git's index and your working tree, so the diff from index to working-tree shows nothing.

Now run:

git reset --hard

which resets to the current commit. This makes Git rip the file some-file out of its index, because a file named some-file is in Git's index right now, but it is not in the (newly-chosen) HEAD commit (which "accidentally on purpose" "just happens" to be the same as the previous HEAD commit, since we didn't pick some other commit to move to). And we used --hard while we did this, so that made Git remove both files.

Now repeat the exercise, but use git reset --mixed first, then git reset --hard. Try to predict the effect.

Finally, repeat the exercise, but before git reset --hard, run git rm --cached some-file.

See also ElpieKay's example of a file that is in the current commit, is not in Git's index at the moment, and is untracked in the working tree. I forgot to cover this case above, but it falls out of the same complications.

  • Related