文件在本地和仓库中都在 .gitignore 中... 如您所见,如果文件已提交,则无效。这是因为 .gitignore 不表示忽略此文件,它的真正意思是:如果它未被跟踪,请不要再提及此文件。如果它已被跟踪,则列出该文件根本不起作用。
我尝试过 git update-index --assume-unchanged 和 git update-index --skip-worktree,但都无济于事。为了确认问题,如果您能准确地说明出错的位置,那将会很有帮助。目前,我假设出错的地方是这些命令似乎有效——它们根本没有抱怨——但稍后的 git fetch && git merge 却抱怨它会覆盖该文件的内容。(您可能拼写为 git pull,但如果是这样,我建议将其拆分为其两个组件 Git 命令,直到您真正理解每个命令自己做什么。)
这是事情变得复杂的地方。我们必须理解Git的提交和合并模型。随之而来的是索引(也称为暂存区或缓存)的作用,以及工作树在git merge期间的作用。
合并的工作原理
首先,让我们快速概述提交和合并过程。您已经知道每个提交都具有所有已提交文件的完整快照,并且每个提交包含其父提交的哈希ID。因此,如果我们在git fetch之后但在git merge运行之前绘制提交图,我们可能会看到这个:
G--H--I <-- master (HEAD)
/
...--F
\
J--K--L <-- origin/master
1如果你还不知道这些内容,请阅读更多关于Git的资料,例如《Git Book》中有关分支的章节。
在这种情况下,
git merge
将会找到两个提交的共同起点,也就是它们共享的提交点,对于你的提交
I
(指向你的名字为
master
)和他们的提交
L
(指向
origin/master
)。在这里,这个点是提交
F
。
接下来,Git会比较提交
F
中保存的内容和你自己最新提交
I
中保存的内容。这告诉Git
你做了什么修改。这个比较与你运行以下命令所看到的比较相同:
git diff --find-renames <hash of F> <hash of I> # what we changed
Git还将比较提交
F
中保存的内容和其最新提交
L
中保存的内容。这告诉Git它们改变了什么。
git diff --find-renames <hash of F> <hash of L> # what they changed
现在你可以看到
git merge
是如何工作的:它将
你所更改的内容 与
他们所更改的内容 结合起来。使用这些合并后的更改,Git提取保存在基础提交(提交
F
)中的内容,并将
合并后的 更改应用于两组更改中所更改的所有文件。如果一切顺利,结果就是快照,应该作为合并提交进行提交;Git将执行此操作,提交合并并调整您当前的分支:
G--H--I
/ \
...--F M <-- master (HEAD)
\ /
J--K--L <-- origin/master
索引和工作树
冻结在提交中的文件存在一个基本问题:它们是(a)被冻结的,以及(b)以一种特殊的、压缩的、仅适用于Git的形式存在。对于源代码管理来说,这个被冻结的部分非常好:这些文件永远不会改变,因此您可以通过检出旧提交来获取以前的工作内容。特殊的压缩的Git-only形式有助于控制存储空间:由于Git保存了每个文件的每个版本,如果它们不是特殊的压缩形式,您可能很快就会用完磁盘空间。但它也带来了一个问题:如何访问冻结的文件?如何更改它们?
Git 的解决方案是工作树。在某个提交上执行git checkout
会展开和解冻保存在该提交中的文件。解冻并重新构建的文件会进入您的工作树,在那里您可以使用它们并更改它们。
在其他版本控制系统中,这就是整个故事的结束:你有冻结的文件,不能更改,还有未冻结的工作树,可以更改。但是Git添加了这种中间形式,Git称之为索引、暂存区或缓存,具体取决于谁/哪个部分的Git在调用它。了解索引对于使用Git至关重要,但很少有人讲解得很好。人们(和IDE)试图掩盖它并将其隐藏起来,但这种方法行不通,而且它行不通的原因很重要——特别是在你的情况下。我知道的最好的描述索引的方法是:它是下一个提交的内容。当Git提取冻结的文件时,它首先只是将它们解冻(更准确地说,将它们收集到一个统一的列表中,该列表未被冻结,与提交中内部的更结构化的冻结列表相反)。这些现在已解冻的副本进入索引。它们仍然是Git-ified,压缩并占用最小的存储空间。
一旦文件被解冻到索引中,Git 才会将其解压成工作树格式。因此,它首先被解冻(索引副本),然后才被提取到工作树中。这意味着索引有一个副本可以准备好被冻结到下一个提交中。
如果您更改了工作树中的文件,则必须在该文件上运行 git add 命令以复制(并压缩和 Git-ify)该文件,使其适合索引。现在,索引副本与工作树副本匹配,除了索引副本是特殊的 Git-only 形式。现在它已经准备好进入下一个提交。
这就是
git status
的工作原理:对于每个文件,它会将工作区副本与索引副本进行比较,如果它们是
不同的,则说明该文件
未准备提交。它还会将索引副本(以特殊的 Git 格式表示)与
HEAD
提交副本进行比较,如果它们是
不同的,则说明该文件已经
准备提交了。因此,如果工作区、索引和
HEAD
提交中有10,000个文件,则实际上总共有30,000个副本(10k x 3个副本)。但是只有
两个文件在这三个副本中
不同,那么只有两个文件会在
git status
中列出(并且其 Git 化的副本相对较小)。
在运行
git commit
之前,索引中不同的文件只是索引中的不同。当你运行
git commit
时,Git会冻结索引——甚至不查看工作树!——并将其作为新的
HEAD
提交。你的新提交现在与索引匹配,因此现在所有索引文件的副本都与它们的`HEAD commit`副本匹配。
(另外:在冲突合并期间,索引承担了扩展角色。现在它不仅持有每个文件的一个副本,而且还可以持有每个文件的三个副本。但我们这里不涉及冲突合并,所以我们不必担心这个。)
< h3 >假定未更改和跳过工作树位< /h3 >
现在我们可以看到这两个位的作用。当你运行
git status
时,Git通常会将每个文件的工作树副本与相同文件的索引副本进行比较。如果它们不同,Git会说您有一个未准备提交的更改。 (如果文件根本不在索引中,Git会说该文件未跟踪,然后
.gitignore
文件就很重要了。但是,如果文件已经在索引中,则该文件已被跟踪,
.gitignore
文件无关紧要。)
如果设置了assume-unchanged或skip-worktree位之一,则
git status
将不会将文件的工作树版本与索引版本进行比较。它们可以完全不同,
git status
将对它们不予理会。
请注意,
git commit
完全忽略这些位!它只是冻结索引副本。如果索引副本与工作树副本匹配,则意味着您在再次提交时保留了文件相同。您的新提交具有与先前提交相同的冻结副本。您的索引副本继续匹配您的
HEAD
提交副本。
当 Git 需要更改文件时,问题就出现了。假设您已设置跳过工作树位(通常应设置此位,因为另一个位用于不同的问题,但实际上任何一个都可以使用)。您还修改了工作树副本。运行
git status
不会报错,因为
git status
不会再比较工作树副本和索引副本了。
但现在你运行了
git merge
,合并想要接受文件的
更改。例如,Git将提交
F
与提交
I
和
L
进行比较,并发现尽管您没有在
I
中提交新版本的文件,
他们已经在
L
中提交了一个新版本的文件。所以Git将采取他们的更改,使这些更改进入新的合并提交
M
,然后...提取
M
到您的工作树,覆盖您的文件副本。
覆盖您的文件是不好的,所以Git不会这样做。相反,它只是失败了合并。
你应该怎么做?
最终,您必须将文件的版本保存在某个地方。这可以在Git内部完成,例如作为提交,也可以在Git之外通过复制文件到存储库之外来完成。然后,您可以将自己的更改与他们的更改合并,或者仅采用他们的版本重新执行自己的更改。
这其实就是
git stash
的作用。它会创建一个提交,实际上是
两个 提交。这些提交的特殊之处在于它们不在
任何 分支上。完成这些提交后,
git stash
运行
git reset --hard
命令来丢弃索引和工作树中对文件所做的更改。你没有索引更改,即使有索引副本也保存在存储堆栈提交中,因此重置命令中的
--mixed
部分是安全的,你的工作树副本也保存在存储堆栈提交中,因此重置命令中的
--hard
部分也是安全的。现在你的索引和工作树已经干净了,可以安全地合并。然后
git stash pop
——实际上是
git stash apply && git stash drop
——可以使用一种不太安全的内部合并方法将你的存储堆栈工作树版本的文件与当前文件版本合并,该方法只适用于工作树副本。
drop
步骤会删除存储堆栈提交,使其成为
未引用2,最终将被完全删除。
这里有几种替代使用
git stash
的方法,但没有一种像它那样简单易懂。你最好还是使用
git stash
。
最后,你可以完全停止提交该文件。一旦文件不再在索引中,它就变成了未跟踪状态,并且不会出现在任何未来的提交中。在我看来,这是最好的解决方案,但它有一个非常大的缺点:该文件过去已经被提交过。这意味着,如果你检出一个包含该文件的旧提交,该文件将同时出现在当前提交(你刚刚检出的旧提交)和索引中,并且将被跟踪。当你从旧提交切换到不包含该文件的新提交时,该文件将被删除!这就是你说的:
我想用git rm --cached
,但根据我所读的资料,似乎这会从存储库中删除该文件...
具体来说,它从索引中删除文件,但保留工作树副本。这不会从存储库中删除文件,但存储库本身主要由提交组成,“从存储库中删除”是一个无意义的短语。你确实无法从现有的提交中删除文件:它们永远冻结在时间上。你只能避免将文件放入未来的提交中。
这个方法可行,但存在陷阱:返回历史提交会将文件恢复到索引中(因为“git checkout commit”意味着“从提交中填充索引并使用它来填充工作树”)。一旦在索引中,它将出现在未来的提交中。切换到没有该文件的提交需要从索引中删除它,这意味着从工作树中删除它,然后工作树副本就不存在了。
因此,如果您想选择这种方式,这是一个不错的选择,您应该:
- 停止使用那个文件:将其重命名为
config.sample
- 切换到一个新的(不同的)文件名用于实际配置,并完全将此文件保留在存储库之外(例如,将其存储在
$HOME/.fooconfig
中)
并将所有内容作为一个提交的一部分,之后旧的配置文件将不再使用。告诉人们在切换到新版本的foo
程序之前将其配置移动到新位置。这是一个主要版本升级,因为行为不同。
2请查看像Git一样思考。