Git checkout无意中删除了未跟踪的文件

Ben*_*nni 1 git

我遇到了一个奇怪的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-untracked或者git stash save -u像@Ashish Mathew猜测的那样

  • 似乎只有文件和文件已经消失,但是在.gitignore第一次提交时还没有,但后来被添加到了它中

tor*_*rek 6

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

确实.

似乎只有文件和文件已经消失,但是在.gitignore第一次提交时还没有,但后来被添加到了它中

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

首先,请注意仅忽略未跟踪的文件

即使.gitignore文件说要忽略它,也不会忽略未跟踪(被跟踪)的文件.仅忽略未跟踪的文件:跟踪文件,未跟踪但未被忽略或未跟踪和忽略的文件.

但是等一下:究竟是什么,是一个未跟踪的文件?

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

这个定义是Git中为数不多的简单明了的定义之一.或者更确切地说,如果清楚指数什么的话.不幸的是,该指数很难看到.

我对索引的最佳一行描述是:*索引是您构建下一个提交的位置.*

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

这意味着您的索引实际上是的非常罕见.大多数情况下,它只匹配您当前的提交.那是因为你刚刚检查了那个提交,它将这些文件放入你的索引(以Git形式,准备好下次提交)和你的工作树(以常规的普通文件形式,准备使用或编辑).

如果修改文件F并运行git add F,则git add 替换之前索引中的文件(Git格式)副本.索引不是空的 - F其中包含其他内容 - 它只是匹配当前提交,因此大多数Git命令都没有提到,F直到你F在工作树中进行了更改.

那么,让我们考虑一下:

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

这告诉Git:从第一次提交填充索引和工作树. 让我们假设我们还没有实际运行这个命令,只考虑:这会做什么?那个提交是什么?

好吧,当你创建它时,该提交具有索引中的任何内容 - 无论你曾经用于git add复制到索引中.这包括,例如,文件abc.txt,你后来决定不得不跟踪.

为了不跟踪,你必须在某个时候从索引中删除 abc.txt,可能是:

git rm --cached abc.txt
Run Code Online (Sandbox Code Playgroud)

(在删除索引副本的同时保留工作树副本).之后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将该提交中的所有文件提取到索引和工作树中.(对于这个一般性的想法,有一些复杂的但是试图安全的例外:当当前分支上有未提交的更改时,请参阅Checkout另一个分支.)并且,嘿,现在abc.txt 在索引中.现在它被追踪了!

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

git checkout <my working branch>

现在Git必须将索引和工作树内容从第一次提交abc.txt切换到提示<my working branch>.这承诺并不具有abc.txt在里面.Git将从索引中删除该文件...并将其从工作树中删除,因为它已被跟踪.

结帐完成后,现在文件不在索引中.好吧,它也不在工作树(argh)中.如果你把它放回到工作树中,现在已经没有了.但你在哪里可以得到它?

答案就是盯着我们:这是第一次提交. 当你运行时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
Run Code Online (Sandbox Code Playgroud)

(请注意,git show默认情况下不会应用行尾转换和涂抹过滤器 - 在现代Git中,你应该可以使用它来实现--textconv).

你必须为Git删除的每个文件执行此操作,这可能相当痛苦.


示例会话:.gitgnore使用clobbering数据使Git OK

我为测试目的制作了一个小存储库.在这个存储库中,我使用包含一行读取的README文件进行了初始提交:abc.txtoriginal

$ 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
Run Code Online (Sandbox Code Playgroud)

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

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

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

$ echo precious > abc.txt
$ git status
On branch master
nothing to commit, working tree clean
$ cat abc.txt   
precious
Run Code Online (Sandbox Code Playgroud)

现在让我们看看提交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
Run Code Online (Sandbox Code Playgroud)

哎呀,我们宝贵的数据已被破坏!

它是.gitignoreGit允许破坏文件的指令.为了证明这一点,让我们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)
Run Code Online (Sandbox Code Playgroud)

现在,如果我们要求切换到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
Run Code Online (Sandbox Code Playgroud)

因此,忽略文件会产生令人讨厌的副作用:它们变得更容易被破坏.我(我认为,与过去的其他人一起)已经开始研究Git"被忽视但可以破坏"和"被忽视但是珍贵,不要破坏"之间的区别,并且无法简单地解决它并且已经放弃了努力.

(我认为Git在某一方面对此表现得更好,但是这个例子表明它至少在Git 2.14.1中仍然很糟糕,这是我在这组测试中使用的版本.)