简体   繁体   English

Git Checkout意外删除了未跟踪的文件

[英]Git checkout has deleted untracked files unintentionally

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. 我遇到了Git的一个奇怪行为:我有一个存储库,其中包含一些在.gitignore文件中指定的未跟踪文件和文件夹。

The exact steps that I made: 我所做的确切步骤:

  1. Stashed 4 files: git stash 存放了4个文件: git stash
  2. Checked out my very first commit from months ago: git checkout <hash of first commit> 签出了我几个月前的第一次提交: 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> 回到我的工作分支进行git checkout <my working branch>
  5. Applied the stash: git stash apply 应用了存储: 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 我没有执行其中一个命令git stash --include-untrackedgit stash save -u ,正如@Ashish Mathew猜测的那样

  • 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 似乎只有文件和文件夹在第一次提交时就消失了,而.gitignore文件中还没有,但是后来又添加了

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 似乎只有文件和文件夹在第一次提交时就消失了,而.gitignore文件中还没有,但是后来又添加了

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 . 不幸的是,您必须将它们全部拼写出来,然后与Git一起大惊小怪,您可能会得到错误的版本 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. 即使一个.gitignore文件说忽略它,也不会忽略未跟踪(被跟踪)的文件。 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. 该定义是Git中为数不多的简单明了的定义之一。 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—ie, indexes—your work-tree. 该索引,也称为暂存区缓存 ,可跟踪您的工作树(即索引)。 Your work-tree is where you do your work: it has your files in their normal, non-Git format. 工作树是您工作的地方:它以正常的非Git格式存储文件。 Files stored permanently and read-only in commits, inside the Git repository, have a special, compressed, Git-only format. 在Git信息库中,永久且只读存储在提交中的文件具有特殊的,压缩的,仅Git格式。 The index "sits in between" these two places: it has all your commit-able files, from your work-tree, all set to be committed . 索引位于这两个位置之间:它具有工作树中所有可提交的文件, 都设置为commit But the files in the index are changeable (unlike those inside commits) even though they're already converted to the special Git format. 但是索引中的文件是可变的 (不同于内部提交中的文件),即使它们已经转换为特殊的Git格式。

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). 那是因为您刚刚签出该提交,这会将这些文件放入索引(仅Git形式,准备进行下一次提交)和工作树(以常规普通文件形式,准备使用或编辑)。

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. 如果修改文件F并运行git add F ,则git add 将替换以前在索引中(Git格式)的文件副本。 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. 该指数是不是空的 -它有F它,一切沿着别人,它只是匹配当前提交 ,所以大多数Git命令不提F直到你改变F在工作树。

So, let's consider: 因此,让我们考虑:

Checked out my very first commit from months ago: git checkout <hash of first commit> 签出了我几个月前的第一次提交: git checkout <hash of first commit>

This tells Git: fill the index and work-tree from that very first commit. 这告诉Git: 从第一次提交开始就填充索引和工作树。 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. 好了,该提交在创建时具有索引中的任何内容,无论您使用git add复制到索引中的是什么。 That includes, say, file abc.txt , which you decided later had to be untracked . 例如,其中包括文件abc.txt ,您后来决定必须将其取消跟踪

To be untracked, you had to remove abc.txt from the index at some point, probably with: 要取消跟踪,必须在某个时候从索引中删除 abc.txt ,可能是:

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 . git rm --cached ,您执行了git commit From the time you ran git rm --cached , until now, the file was not in the index. 从您运行git rm --cached到现在,该文件不在索引中。 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签出您的第一个提交,但是...很好,该第一个提交中包含abc.txt Git needs to copy the committed version of abc.txt into the index and into the work-tree. Git需要将提交的abc.txt版本复制到索引工作树中。

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 . 此时,如果工作树中已经有一个abc.txt ,那么Git将检查您是否要使用其他abc.txt对其进行破坏。 Mostly, Git will refuse to do so, telling you to move it out of the way first. 通常,Git会拒绝这样做,告诉您先将其移开。 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. 但是,如果工作树中的abc.txt与提交中的abc.txt相匹配,那么可以安全地使用提交中的abc.txt填充索引。 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. 因此,在这一点上,Git从该提交中提取所有文件,并将其提取到索引和工作树中。 (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. (有一些复杂,但试图将要安全的,例外的总体思路:看结帐时,有对当前分支未提交的更改另一个分支 。)而且,哇哎,现在abc.txt 在索引中。 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> . 现在,Git必须将索引和工作树的内容从其中包含abc.txt的第一个提交切换到<my working branch>的尖端提交。 That commit doesn't have abc.txt in it. 该提交中没有 abc.txt Git will remove the file from the index ... and remove it from the work-tree too, because it's tracked . Git将从索引中删除该文件...,并将其也从工作树中删除,因为它已被跟踪

Once the checkout finishes, now the file isn't in the index. 签出完成后, 现在文件不在索引中。 Well, it also isn't in the work-tree ( argh ). 嗯,它也不在工作树( 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). 当您运行git checkout <hash> ,Git会将文件复制到索引和工作树中(除非它毕竟不必触摸工作树版本)。 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> . 当您运行git checkout <my working branch>取回文件时,Git 删除了该文件,但是提交是只读的,并且(通常)是永久的,因此该文件仍然以Git-only的形式存在于提交<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. 诀窍是使它脱离 commit <hash> 而不将其放回索引中,从而使其以普通的非Git格式存在。 The easy way to do this these days is to use git show hash : path > path , eg: 这几天最简单的方法是使用git show hash : path > path ,例如:

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 ). (请注意,默认情况下git show并不应用行尾翻译和污迹过滤器-在现代Git中,您应该可以使用--textconv--textconv )。

You will have to do this for every file that Git removed, which can be rather painful. 您将必须为Git删除的每个文件执行此操作,这可能会很痛苦。


Example session: .gitgnore makes Git OK with clobbering data 会话示例: .gitgnore通过破坏数据使Git正常运行

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 : 在该存储库中,我使用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

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 : 让我们在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 : 现在,让我们检查一下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. .gitignore指令赋予Git破坏文件的权限。 To prove this, let's make abc.txt not-ignored (but also not tracked): 为了证明这一点,让我们使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)

Now if we ask to switch to initial : 现在,如果我们要求切换到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. 我(与过去的其他人一起)一直在研究向Git教授“忽略并可能破坏”和“忽略但珍贵,不要破坏”之间的区别,并且无法简单地解决它并放弃了努力。

(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在这一点上表现得更好,但是此示例表明,至少在Git 2.14.1(这是我在这组特定测试中使用的版本)上,它仍然很糟糕。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM