存储的文件与消失的文件无关......确实。
似乎消失的只有那些在第一次提交时不在.gitignore中但之后被添加到其中的文件和文件夹。这加上另一件事,(几乎肯定)是问题的根源。幸运的是,你应该能够找回那些文件——或者至少是它们的某个版本。不幸的是,你需要把它们的名称一个个列出来并大费周折地使用Git,并且可能会得到错误的版本。见底部的示例会话。
首先,请注意,只有未跟踪的文件会被忽略。
即使.gitignore文件指示要忽略已跟踪的文件,也不会被忽略。只有未跟踪的文件会被忽略:文件可以是已跟踪、未跟踪但未被忽略,或未跟踪且被忽略。
但是等等,什么是未跟踪的文件呢?
未跟踪的文件是不在索引中的文件。
这个定义是Git中为数不多的简单明了的定义之一。或者说,如果能清楚索引是什么,那么它将非常清晰。不幸的是,索引非常难以看到。
我对索引的最好描述是这个:索引是你构建下一个提交的地方。
这个索引,也称为缓存区和暂存区,跟踪(即索引)你的工作目录。你的工作目录是你进行工作的地方:它有你的文件以正常的非Git格式存在。永久存储和只读存储在提交中,在Git存储库内部的文件具有特殊的、压缩的、仅限于Git的格式。索引“坐在”这两个位置之间:它拥有你所有可提交的文件,来自你的工作目录,全部准备好被提交了。但是索引中的文件是可变的(与提交内部的不同),即使它们已转换为特殊的Git格式。
这意味着你的索引很少是空的。大多数时候,它只是匹配你当前的提交。那是因为你刚刚检出了那个提交,将那些文件放入你的索引(以Git-only形式,准备下一次提交)和你的工作目录(以普通的文件形式,准备使用或编辑)。如果您修改了文件
F
并运行
git add F
,则
git add
将替换在索引中之前(以Git格式)存在的文件副本。索引不是空的 - 它包含
F
和其他所有内容 - 它只是匹配了当前提交,因此大多数Git命令在您在工作树中更改
F
之前不提到
F
。
所以,让我们考虑:
检出我几个月前的第一个提交:
git checkout <hash of first commit>
这告诉Git:从那个非常初期的提交中填充索引和工作树。假设我们尚未实际运行此命令,只需考虑:这会做什么?那个提交中有什么?
嗯,当您进行提交时,提交中有您添加到索引中的任何内容。这包括您稍后决定必须将其
取消跟踪的文件
abc.txt
。
要取消跟踪,您必须在某个时候从索引中
删除abc.txt
,可能使用以下命令:
git rm --cached abc.txt
(在保留工作目录副本的同时,删除索引副本)。在运行 git rm --cached
之后,您执行了 git commit
。从运行 git rm --cached
到现在,该文件在索引中不存在。它在工作树中。因此,它是未跟踪的。
检出任何提交都会从该提交填充索引
现在,您告诉Git要检出您的第一个提交了...那么,第一次提交包含 abc.txt
。Git需要将 abc.txt
的提交版本复制到索引中并且 复制到工作树中。
此时,如果工作树中已经有一个 abc.txt
,Git将检查您是否要用不同的 abc.txt
覆盖它。大多数情况下,Git将拒绝这样做,并告诉您先将其移开。 但是,如果工作树中的 abc.txt
与提交中的匹配,则可以安全地使用来自提交的 abc.txt
填充索引。毕竟,它与工作树中的文件匹配。
因此,在此时,Git将所有文件从该提交中提取到索引和工作树中。 (有一些复杂但试图安全的例外情况:请参见当当前分支存在未提交的更改时检出另一个分支)。并且,哇喂,现在 abc.txt
在索引中。 现在它被跟踪了!
因此,现在您查看旧提交,并决定:
git checkout <my working branch>
现在Git必须从第一个提交中切换索引和工作树内容,该提交包含 abc.txt
,到<my working branch>
的尖端提交中。该提交中没有 abc.txt
。 Git将从索引中删除文件...并从工作树中删除它,因为它是已跟踪的。
一旦检出完成,现在该文件不在索引中。好吧,它也不在工作树中。(啊)。如果您将其放回到工作树中,现在它就是未跟踪的。但是,您可以从哪里获取它呢?
答案就在我们眼前:它就在那个第一次提交里。当你运行
git checkout <hash>
时,Git 把文件复制到了索引和工作目录中(除了它最终并没有必要再次触及工作目录中的版本)。当你运行
git checkout <my working branch>
回到工作分支时,Git 会将文件删除,但提交是只读的,而且(大多数情况下)是永久的,所以文件仍然以 Git-Only 的形式存在于提交
<hash>
中。
诀窍是把它从提交
<hash>
中拿出来,
而不是将它放回到索引中,以便以正常的、非 Git 格式留存。如今实现这一点的简单方法是使用
git show hash:path > path
,例如:
git show hash:abc.txt > abc.txt
(请注意,默认情况下 git show
不会应用行尾翻译和smudge过滤器,但在现代Git中,您应该能够使用 --textconv
实现这一点)。
您将不得不针对 Git 删除的每个文件执行此操作,这可能相当麻烦。
示例会话:.gitgnore
使Git能够覆盖数据
我为测试目的创建了一个微小的存储库。 在此存储库中,我进行了初始提交,其中包含一个 README
文件和一个包含一行文本“original”的 abc.txt
文件:
$ 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中仍然存在问题,而这正是我在这组测试中使用的版本。)