Git Checkout意外删除了未跟踪的文件 [英] Git checkout has deleted untracked files unintentionally

查看:80
本文介绍了Git Checkout意外删除了未跟踪的文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我遇到了Git的奇怪行为: 我有一个存储库,其中包含许多.gitignore文件中指定的未跟踪文件和文件夹.

我执行的确切步骤:

  1. 隐藏了4个文件:git stash
  2. 检查了我几个月前的第一次提交:git checkout <hash of first commit>
  3. 环顾四周,保持不变
  4. 回到我的工作分支,做git checkout <my working branch>
  5. 应用了隐藏项:git stash apply

然后,我注意到一些(不是全部)未跟踪的文件和文件夹消失了.怎么可能呢?

其他信息:

  • 隐藏的文件与消失的文件无关,我注意到了隐藏操作只是为了完整性

  • 我没有执行命令git stash --include-untrackedgit stash save -u之一,如@Ashish Mathew所猜测的

  • 似乎只有文件和文件夹消失了,这些文件和文件夹在第一次提交时还没有出现在.gitignore中,但后来又被添加到其中了

解决方案

隐藏的文件与消失的文件无关...

确实.

似乎只有文件和文件夹在第一次提交时就消失了,而这些文件和文件夹在.gitignore中还没有出现,但后来又被添加到了文件中

(几乎可以肯定)这是问题的根源.幸运的是,您应该能够取回这些文件,或者至少取回这些文件的某些版本.不幸的是,您必须将它们全部拼写出来并与Git一起大惊小怪,您可能会得到错误的版本.请参阅底部的示例会话.

首先,请注意,只有未跟踪的文件会被忽略

未被跟踪(未跟踪)的文件永远不会被忽略,即使.gitignore文件说要忽略它.只会忽略未跟踪的文件:已跟踪文件,未跟踪但未被忽略的文件或未跟踪并被忽略的文件.

但是等等:未跟踪的文件到底是什么?

未跟踪的文件是不在索引中的文件

此定义是Git中为数不多的简单明了的定义之一.或者,确切地说,索引是什么.不幸的是,该索引很难查看.

我对索引的最好的一行描述是:*索引是构建 next 提交要进行的地方.*

此索引,也称为临时区域缓存,用于跟踪工作树,即索引.工作树是您工作的地方:文件具有其正常的非Git格式的文件.在Git信息库中,永久且只读存储在提交中的文件具有特殊的,压缩的,仅Git格式.索引位于这两个位置之间:它具有工作树中所有可提交的文件,都已设置为要提交.但是索引中的文件是可更改的(不同于内部提交中的文件),即使它们已经转换为特殊的Git格式.

这意味着您的索引实际上为的情况非常罕见.在大多数情况下,它仅与您当前的提交匹配.这是因为您刚刚签出该提交,这会将这些文件放入索引(仅Git形式,准备进行下一次提交)和工作树(以常规普通文件形式,准备使用或编辑). /p>

如果您修改文件F并运行git add F,则git add 替换之前位于索引中的文件副本(采用Git格式).索引不是 empty -它具有F以及其他所有内容-它仅与当前提交相匹配,因此大多数Git命令没有提及F,直到您在工作树中更改了F.

因此,让我们考虑:

检查了我几个月前的第一次提交:git checkout <hash of first commit>

这告诉Git:从第一次提交开始就填充索引和工作树.让我们假设我们实际上还没有运行此命令,而只是考虑:这将做什么?该提交中包含什么内容?

好吧,提交时该提交具有索引中的任何内容,无论您使用git add复制到索引中的是什么.例如,其中包括文件abc.txt,您后来决定将其取消跟踪.

要取消跟踪,您必须在某个时候从索引中删除 abc.txt,可能是:

git rm --cached abc.txt

(将工作树副本保留在原处,同时删除索引副本). git rm --cached之后,您进行了git commit.从您运行git rm --cached到现在,该文件一直在索引中 not 中.它在工作树中.所以它是未跟踪.

检出任何提交将填充该提交的索引

现在,您已经告诉Git签出您的第一个提交,但是...很好,那个第一个提交包含abc.txt. Git需要将提交的abc.txt版本复制到索引中,并将复制到工作树中.

这时,如果工作树中已经有一个abc.txt,则Git将检查您是否要使用其他的abc.txt破坏它.通常,Git会拒绝这样做,告诉您先将其移开.但是,如果工作树中的abc.txt与提交中的abc.txt相匹配,那么可以安全地使用提交中的abc.txt填充索引.毕竟,它与工作树中的一个匹配.

因此,在这一点上,Git从该提交中提取所有文件,索引和工作树. (此一般性想法有一些复杂但尝试安全的例外情况:请参见在未提交更改的情况下签出另一个分支在当前分支上.),嘿,现在abc.txt 在索引中.现在已被跟踪!

所以现在您环顾四周,看看您的旧提交,并决定:

git checkout <my working branch>

现在,Git必须将索引和工作树的内容从其中包含abc.txt的第一个提交切换到<my working branch>的尖端提交.该提交没有包含abc.txt. Git将从索引中删除文件...并将其也从工作树中删除,因为它已被跟踪.

结帐完成后,现在文件中的不是在索引中.嗯,它也不在工作树中( argh ).如果将其放回工作树中,则 now 不会被跟踪.但是,在哪里可以得到它?

答案正盯着我们:它在第一次提交中.当您运行git checkout <hash>时,Git将文件复制到索引和工作树中(除了它没有毕竟不必触摸工作树版本).当您运行git checkout <my working branch>取回文件时,Git 删除了,但是提交是只读的,并且(通常)是永久的,因此该文件仍以纯Git的形式存在,并且以<hash>.

诀窍是在提交<hash> 而没有的情况下将其取出,并将其放回索引中,以便其以普通的非Git格式显示.目前,执行此操作的简单方法是使用git show hash:path > path,例如:

git show hash:abc.txt > abc.txt

(请注意,默认情况下,git show不会应用行尾翻译和污迹过滤器-在现代Git中,您应该可以使用--textconv使其生效).

您将必须对Git删除的每个文件执行此操作,这可能会很痛苦.


会话示例:.gitgnore使用破坏性数据使Git正常

我为测试目的建立了一个很小的存储库.在此存储库中,我使用README和文件abc.txt进行了一次初始提交,其中包含一行读取original的文件:

$ mkdir tt
$ cd tt
$ git init
Initialized empty Git repository in ...
$ echo original > abc.txt
$ echo for testing overwrite > README
$ git add README abc.txt
$ git commit -m initial
[master (root-commit) a721a23] initial
 2 files changed, 2 insertions(+)
 create mode 100644 README
 create mode 100644 abc.txt
$ git tag initial
$ git rm abc.txt
rm 'abc.txt'
$ git commit -m 'remove abc'
[master 20ba026] remove abc
 1 file changed, 1 deletion(-)
 delete mode 100644 abc.txt
$ touch unrelated.txt
$ echo abc.txt > .gitignore
$ git add .gitignore unrelated.txt 
$ git commit -m 'add unrelated file and ignore rule'
[master 067ea61] add unrelated file and ignore rule
 2 files changed, 1 insertion(+)
 create mode 100644 .gitignore
 create mode 100644 unrelated.txt

我们现在有了一个包含三个提交的存储库:

$ git log --oneline --decorate
067ea61 add unrelated file and ignore rule
20ba026 remove abc
a721a23 (tag: initial) initial

让我们将一些珍贵的数据放入(忽略)abc.txt:

$ echo precious > abc.txt
$ git status
On branch master
nothing to commit, working tree clean
$ cat abc.txt   
precious

现在让我们检查一下initial提交:

$ git checkout initial
Note: checking out 'initial'.

You are in 'detached HEAD' state. [mass snip]

HEAD is now at a721a23... initial
$ cat abc.txt
original

糟糕,我们的宝贵数据已被破坏!

.gitignore指令,它赋予Git破坏文件的权限.为了证明这一点,让我们使abc.txt不被忽略(但也不会被跟踪):

$ cp /dev/null .gitignore
$ git add .gitignore
$ git commit -m 'do not ignore precious abc.txt'
[master 564c4fd] do not ignore precious abc.txt
 Date: Thu Feb 8 14:16:08 2018 -0800
 1 file changed, 1 deletion(-)
$ git log --oneline --decorate
564c4fd (HEAD -> master) do not ignore precious abc.txt
067ea61 add unrelated file and ignore rule
20ba026 remove abc
a721a23 (tag: initial) initial
$ echo precious > abc.txt
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    abc.txt

nothing added to commit but untracked files present (use "git add" to track)

现在,如果我们要求切换到initial:

$ git checkout initial
error: The following untracked working tree files would be overwritten by checkout:
    abc.txt
Please move or remove them before you switch branches.
Aborting

因此忽略文件有一个烦人的副作用:文件变得容易被破坏.我(与过去的其他人一起)一直在研究向Git教授忽略并可能破坏"和忽略但珍贵,不要破坏"之间的区别,并且无法简单地解决它并放弃了努力.

(我认为Git在这一点上表现得更好,但是此示例表明,至少在Git 2.14.1(这是我在这组特定测试中使用的版本)上,它仍然很糟糕.)

I have encountered a strange behaviour of Git: I have a repository that contains a number of untracked files and folders specified in the .gitignore file.

The exact steps that I made:

  1. Stashed 4 files: git stash
  2. Checked out my very first commit from months ago: git checkout <hash of first commit>
  3. Looked around without changing anything
  4. Went back to my working branch doing git checkout <my working branch>
  5. Applied the stash: git stash apply

Then I noticed that some (not all) of my untracked files and folders have gone away. How can that be?

Additional info:

  • The stashed files have nothing to do with the disappeared files, I noted the stash actions just for completeness

  • I did not perform one of the commands git stash --include-untracked or git stash save -u, as @Ashish Mathew guessed

  • It seems that only files and foldes have disappeared that were not yet in the .gitignore at the first commit, but have been later added to it

解决方案

The stashed files have nothing to do with the disappeared files ...

Indeed.

It seems that only files and foldes have disappeared that were not yet in the .gitignore at the first commit, but have been later added to it

This, plus one more thing, is (almost certainly) the source of the problem. Fortunately, you should be able to get those files back—or at least some version of those files. Unfortunately, you'll have to spell them all out and fuss with Git a bunch, and you may get the wrong version. See the example session at the bottom.

First, note that only untracked files are ignored

A file that is not untracked (that is tracked) is never ignored, even if a .gitignore file says to ignore it. Only untracked files are ignored: files are either tracked, untracked-but-not-ignored, or untracked-and-ignored.

But wait: what, precisely, is an untracked file?

An untracked file is a file that is not in the index

This definition is one of the few in Git that is simple and clear. Or, rather, it would be if it were clear what the index is. Unfortunately, the index is very hard to see.

The best one line description I have for the index is this: *The index is where you build your next commit to make.*

This index, also called the staging area and the cache, keeps track of—i.e., indexes—your work-tree. Your work-tree is where you do your work: it has your files in their normal, non-Git format. Files stored permanently and read-only in commits, inside the Git repository, have a special, compressed, Git-only format. The index "sits in between" these two places: it has all your commit-able files, from your work-tree, all set to be committed. But the files in the index are changeable (unlike those inside commits) even though they're already converted to the special Git format.

This means that it's very rare for your index to actually be empty. Most of the time, it just matches your current commit. That's because you just checked out that commit, which put those files into both your index (in Git-only form, ready for the next commit) and your work-tree (in regular ordinary file form, ready for use or editing).

If you modify a file F and run git add F, the git add replaces the copy of the file that was (in Git format) in the index before. The index wasn't empty—it had F in it, along with everything else—it just matched the current commit, so most Git commands don't mention F until you've changed F in the work-tree.

So, let's consider:

Checked out my very first commit from months ago: git checkout <hash of first commit>

This tells Git: fill the index and work-tree from that very first commit. Let's suppose we have not actually run this command yet, and just consider: what will this do? What's in that commit?

Well, that commit has whatever was in the index when you made it—whatever you had used git add to copy into the index. That includes, say, file abc.txt, which you decided later had to be untracked.

To be untracked, you had to remove abc.txt from the index at some point, probably with:

git rm --cached abc.txt

(which leaves the work-tree copy in place, while removing the index copy). After the git rm --cached, you did a git commit. From the time you ran git rm --cached, until now, the file was not in the index. It was in the work-tree. So it was untracked.

Checking out any commit fills in the index from that commit

Now that you have told Git to check out your very first commit, though ... well, that very first commit has abc.txt in it. Git needs to copy the committed version of abc.txt into the index and into the work-tree.

At this point, if there already is an abc.txt in the work-tree, Git will check whether you are going to clobber it with a different abc.txt. Mostly, Git will refuse to do so, telling you to move it out of the way first. But if the abc.txt in the work-tree matches the one in the commit, well, then it's safe to fill in the index with the abc.txt from the commit. It matches the one in the work-tree, after all.

So at this point, Git extracts all the files from that commit, into the index and into the work-tree. (There are some complicated, but attempted-to-be-safe, exceptions to this general idea: see Checkout another branch when there are uncommitted changes on the current branch.) And, whoa hey, now abc.txt is in the index. Now it's tracked!

So now you look around and at your old commit, and decide to:

git checkout <my working branch>

and now Git has to switch the index and work-tree contents from the first commit, which has abc.txt in it, to the tip commit of <my working branch>. That commit doesn't have abc.txt in it. Git will remove the file from the index ... and remove it from the work-tree too, because it's tracked.

Once the checkout finishes, now the file isn't in the index. Well, it also isn't in the work-tree (argh). If you put it back into the work-tree, now it's untracked. But where can you get it?

The answer is staring us in the face: it's in that first commit. When you ran git checkout <hash>, Git copied the file into both the index and the work-tree (except that it didn't have to touch the work-tree version after all). When you ran git checkout <my working branch> to get back, Git removed the file, but commits are read-only and (mostly) permanent, so the file is still there, in Git-only form, in commit <hash>.

The trick is to get it out of commit <hash> without putting it back into the index, so that it sticks around in normal, non-Git format. The easy way to do this these days is to use git show hash:path > path, e.g.:

git show hash:abc.txt > abc.txt

(note that git show by default does not apply end of line translations and smudge filters—in modern Git you should be able to make it do so using --textconv).

You will have to do this for every file that Git removed, which can be rather painful.


Example session: .gitgnore makes Git OK with clobbering data

I made a tiny repository for test purposes. In this repository, I made an initial commit with a README and file abc.txt containing one line reading original:

$ mkdir tt
$ cd tt
$ git init
Initialized empty Git repository in ...
$ echo original > abc.txt
$ echo for testing overwrite > README
$ git add README abc.txt
$ git commit -m initial
[master (root-commit) a721a23] initial
 2 files changed, 2 insertions(+)
 create mode 100644 README
 create mode 100644 abc.txt
$ git tag initial
$ git rm abc.txt
rm 'abc.txt'
$ git commit -m 'remove abc'
[master 20ba026] remove abc
 1 file changed, 1 deletion(-)
 delete mode 100644 abc.txt
$ touch unrelated.txt
$ echo abc.txt > .gitignore
$ git add .gitignore unrelated.txt 
$ git commit -m 'add unrelated file and ignore rule'
[master 067ea61] add unrelated file and ignore rule
 2 files changed, 1 insertion(+)
 create mode 100644 .gitignore
 create mode 100644 unrelated.txt

We now have a repository with three commits:

$ git log --oneline --decorate
067ea61 add unrelated file and ignore rule
20ba026 remove abc
a721a23 (tag: initial) initial

Let's put some precious data in (ignored) abc.txt:

$ echo precious > abc.txt
$ git status
On branch master
nothing to commit, working tree clean
$ cat abc.txt   
precious

Now let's check out commit initial:

$ git checkout initial
Note: checking out 'initial'.

You are in 'detached HEAD' state. [mass snip]

HEAD is now at a721a23... initial
$ cat abc.txt
original

Oops, our precious data has been clobbered!

It's the .gitignore directive that gives Git permission to clobber the file. To prove this, let's make abc.txt not-ignored (but also not tracked):

$ cp /dev/null .gitignore
$ git add .gitignore
$ git commit -m 'do not ignore precious abc.txt'
[master 564c4fd] do not ignore precious abc.txt
 Date: Thu Feb 8 14:16:08 2018 -0800
 1 file changed, 1 deletion(-)
$ git log --oneline --decorate
564c4fd (HEAD -> master) do not ignore precious abc.txt
067ea61 add unrelated file and ignore rule
20ba026 remove abc
a721a23 (tag: initial) initial
$ echo precious > abc.txt
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    abc.txt

nothing added to commit but untracked files present (use "git add" to track)

Now if we ask to switch to initial:

$ git checkout initial
error: The following untracked working tree files would be overwritten by checkout:
    abc.txt
Please move or remove them before you switch branches.
Aborting

So there's an annoying side effect to ignoring files: they become clobber-able. I (along, I think, with others in the past) have looked into teaching Git the difference between "ignored and can clobber" and "ignored but precious, do not clobber" and have not been able to fix it simply and have abandoned the effort.

(I thought at one point Git got better-behaved about this, but this example shows that it is still bad in at least Git 2.14.1, which is the version I used in this particular set of tests.)

这篇关于Git Checkout意外删除了未跟踪的文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆