在.gitignore 中列出一个文件并不意味着忽略该文件,也不意味着不要删除该文件,或者您想要的任何事情喜欢它的意思。您在这里所做的任何事情都不会改变这一点。我们将在这个答案的结尾处回到.gitignore,但让我们先看看你所处的可怕、可怕、糟糕的情况,你真的无法修复。你必须以某种方式绕过它。
这里出了什么问题
事实是,一些现有的提交有这些文件,以及一些其他现有的提交——那些在这些文件存在之前发生的,以及你在你取出文件之后所做的那些 /em> 的 Git 索引——没有这些文件。没有什么可以改变这些事实。
要了解为什么会这样,让我们注意 Git 不是 关于 文件。 Git 是关于提交。提交确实存储文件,但它是一揽子交易:全有或全无。你有一个提交,你有它的所有文件。或者,您没有提交,也没有它的文件(因此您将使用 git fetch 将提交添加到您的集合中,然后您拥有它和所有它的文件)。
此外,提交中的文件是无用的格式(我们将在下一节中讨论)。它们被压缩和去重,因为大多数提交大多具有在其他提交中的 same 文件。所以 Git 不会将它们存储为 文件,而是存储为内部 对象,它会自动对它们进行重复数据删除。
这些对象都有编号,Git 称之为 hash ID。特别是提交总是得到一个 唯一 编号。 (文件,可能是其他提交文件的副本,可能有非唯一的数字,这是对它们进行去重的原因。)这个数字实际上是内部对象内容的加密哈希。这限制了 Git:甚至 Git 也无法更改提交。
如果您取出一个提交,进行一些更改,然后放回不同的东西,它就会不同,因此会得到一个新的和不同的哈希 ID。现有对象保留在 Git 存储库中,在其现有 ID 下。新的和改进的(我们希望)对象现在添加到存储库中,在它的新 ID 下。任何使用 old ID 的人都会得到旧对象。任何使用 new ID 的人都会获得新对象。这部分真的很简单。
现在,提交中的数据只是每个文件的快照。在那里,是的,但也有一些元数据,或者关于提交本身的信息。例如,这包括提交人的姓名和电子邮件地址。它还包括一个时间戳——这有助于确保每个提交都是完全唯一的,因此如果两个不同的人进行 same 提交,但由于某种原因,两个人都声称是相同的 人,他们仍然会得到不同的commits,除非他们同时进行(在这种情况下,他们真的是两个不同的人吗?Git 说不)。
因此,每个提交中都有所有这些元数据:作者、提交者、一些时间戳、日志消息等。但在这些元数据中,Git 添加了自己的信息。 Git 会在每次提交时存储一组 较早 提交的哈希 ID。大多数提交只存储一个哈希 ID,Git 将其称为 父提交。
这些父提交哈希 ID 将提交形成回溯链。我们从最近(最后)提交的右侧开始。我们不会写出它的真实哈希 ID,而是将此提交称为 H(对于 Hash):
<-H
Commit H 包含快照(文件)和元数据,并且在该元数据中,commit H 存储早期提交的哈希 ID。让我们假设它的哈希 ID 是G,然后把它画进去:
<-G <-H
当然,commit G 指向一个更早的提交,它一直向后指向:
... <-F <-G <-H
因为每个提交都向后指向其父提交,所以如果 Git 可以在链中找到最后一个提交,Git 可以并且将会找到整个提交链。这就是分支名称的来源:Git 中的每个名称(分支名称、标签名称、远程跟踪名称等)都存储一个哈希 ID。对于分支名称,该哈希 ID 是其链中 last 提交的 ID。即使链中还有更晚的提交也是如此,这通常在开发新东西但尚未将其放在主分支上时发生:
...--G--H <-- main
\
I--J <-- feature
这里,feature 分支比 main 分支多两个提交。提交J 指向I,后者又指向H,又指向G,以此类推。所以 H 和更早的提交都在 both 分支上。提交 I 和 J 目前仅在 feature 上,但如果我们愿意,我们可以“滑动名称 main 向前”:
...--G--H--I--J <-- main, feature
现在所有提交都在两个分支上。
分支名称移动,每个名称根据定义挑选出被视为“在该分支上”的 last 提交。 提交自己决定了这些分支上的较早内容。所以重要的是 commits:名称只是让我们找到特定的。而且,请记住,所有 提交一直被冻结。任何现有提交的任何部分都不能更改。
检查提交
如上所述,提交中的文件采用只有 Git 可以使用的格式。即使那样,Git 也只能读取它们。我们需要其他程序能够读取和写入到我们的文件。解决方案很简单,与其他版本控制系统使用的相同:Git 在某个时间点将文件从提交中复制出来。这些副本一旦脱离版本控制系统,现在就很有用了。事实上,它们只是普通的普通文件:计算机上的任何东西都可以使用它们。 Git 不再控制它们。
让 Git 从某个提交中复制文件的正常日常方法是使用 git checkout。例如,如果我们有:
...--G--H <-- main
\
I--J <-- feature
我们运行git checkout main,Git 将从提交H 中复制所有已提交的文件。这也有选择名称main 作为我们的当前分支 的副作用。由于名称 main 指向提交 H,这意味着 H 是我们的当前提交。我们可以通过将特殊名称 HEAD 附加到名称 main 来绘制:
...--G--H <-- main (HEAD)
\
I--J <-- feature
请注意,我们现在每个文件都有两个副本:H 中的已提交一个,我们无法触及,还有一个普通格式的日常文件,Git 称之为我们的工作树或工作树。
在其他版本控制系统中,文件的这两个副本是您能找到的唯一副本。1如果您想知道发生了什么,您可以比较一些文件的工作树版本文件到活动的提交版本:任何不同的地方都是我们改变的。但无论出于何种原因——无论你是否认为这是一个好主意2——Git 将每个文件的第三个副本3存储在Git 以不同的方式调用 index,或 暂存区,或者(目前很少见)cache。
每个文件的第三个副本位于只读提交副本和工作树副本之间。与提交的副本不同,它可以被覆盖。它已经过预压缩和预去重,以便准备好进入 next 提交。事实上,这可能是考虑 Git 的索引/暂存区域的最佳通用方式:它保存您的下一次提交。4
所以,当你 git checkout 一些提交时,比如提交 H,Git:
- 从提交中填写其索引,以便您提议的 next 提交匹配;
- 使用这些相同的文件来填充您的工作树,以便您可以查看和使用您的文件;和
- 将
HEAD 附加到分支名称,假设您使用像main 这样的分支名称来选择提交。 (如果没有,您将进入“分离 HEAD”模式,我们不会在此提及。)
如果您现在对文件的工作树副本进行更改,通常还必须运行 git add:这告诉 Git使索引副本与工作树副本匹配。对于您就地更新的文件,这会用新的索引副本覆盖旧的索引副本。对于您删除的文件,这删除索引副本。对于新文件,这会在 Git 的索引中创建一个新文件。
无论哪种方式,添加文件暂存更改,因为无论何时运行git commit,Git 都会从当时索引中的任何内容制作其新快照。如果您没有更改索引,则新快照将与当前快照完全匹配。在这种情况下,Git 通常要求您使用 --allow-empty 标志:新的提交实际上并不是空,只是它匹配旧的快照方式(所以 Git 想知道:为什么要打扰?并让你使用标志)。
无论您是否对工作树进行任何更改和/或运行 git add 以更新 Git 的索引 来自您的工作树,当前提交仍然存在不变。一旦你做了一个新的提交,Git:
- 收集元数据;
- 写出快照和元数据,得到一个哈希ID作为结果;和
- 将哈希ID写入当前分支名称。
我们最终得到,例如:
K <-- main (HEAD)
/
...--G--H
\
I--J <-- feature
现在main 上的提交不在feature 上。
1非当前提交中的其他只读副本也可以找到,就像它们在 Git 中一样,但它们不是 活动当前提交是。
2其他系统没有索引,证明没有索引也可以工作。
3这个“副本”是预先去重的,所以大多数时候,它几乎不占用空间。因此,将其称为 copy 有点误导。然而,与向用户展示的许多其他 Git 位不同,这个“副本”被自动删除重复数据这一事实是非常隐蔽的。您可以将其视为每个文件的第三个副本,并且一切正常。好吧,直到你开始摆弄像git ls-files --stage 和git update-index 这样的内部命令:然后你需要了解git hash-object。
4索引在冲突合并期间扩展,这意味着此描述不完整,但至少没有错误。 :-) 索引还有助于使 Git 运行得更快,这就是它具有旧名称 cache 的原因。这些天你大多在选项标志中看到这个名字,比如git rm --cached。
在具有不同文件的提交之间切换
假设在提交H 和提交I 之间我们删除 一个文件。再进一步说,我们把它放在一个新的分支 X 上:
git checkout main
git checkout -b X
git rm somefile
git commit -m 'remove a file'
提交H有一个名为somefile的文件并提交I缺少一个名为somefile的文件。
当我们git checkout main 时,文件somefile 必须回来。 Git 将其从提交 H 复制到 Git 的索引和我们的工作树,现在我们有了文件。
当我们git checkout X 移回提交I 时,文件somefile 必须离开。 Git 将它从 Git 的索引 和我们的工作树中删除。
此属性由两次提交中的文件集决定。我会说完全,但如果您尝试一下,您会发现 Git 的 删除文件somefile是有条件的:
git checkout main # file somefile comes back
git rm --cached somefile # take somefile out of Git's index
因为我们在这里使用git rm --cached,Git 会从其索引中删除somefile,但不会触及我们的工作树副本。如果我们现在运行:
git checkout X
——记住,提交I,通过分支名称X选择,缺少文件somefile——Git 不会从我们的删除somefile工作树。原因是在git rm --cached 之后,文件somefile未被跟踪。
未跟踪的文件
一个未跟踪的文件,在 Git 中,只是一个文件,它现在在你的工作树中,但现在不在 Git 的索引中。就是这样——这就是整个定义——但它有很多后果,包括git commit 不会在新提交中包含未跟踪的文件,以及我们刚刚看到的缺少removal。
因为您的工作树是您的,您可以随时在其中创建和销毁文件。
因为 Git 的索引是 Git 的,所以 Git 可以将文件放在那里——但我们知道 什么时候它会做哪件事:
-
当您git add 一个文件时,Git 会根据该文件在您的工作树中的样子添加或删除该文件。
-
当您git checkout 提交时,Git 会根据这些文件是否在另一个提交中,将文件添加到索引或从索引中删除。
-
当您运行 git rm --cached 时,Git 会按照指示从 Git 的索引中删除文件。
-
此处未涉及的其他情况包括git merge 如何操作Git 的索引、git reset 和git restore 如何工作等等。
因此,在某种程度上,您控制了 Git 索引中的文件——但它们往往会镜像提交。
索引和工作树特定于每个克隆
对于索引和工作树是否包含在存储库中,Git 有点矛盾。具体来说,git init --bare 创建了一个没有工作树的存储库,但这样的存储库仍然有一个索引。 (可能不应该,但确实如此。)还有 git worktree add 命令,从 Git 2.5 开始,它向存储库添加一对工作树和索引。因此,任何给定的存储库中都可以有多个索引和工作树集。
不过,很明显,git clone 不会复制任何现有存储库的索引和工作树(无论该存储库中有多少个)。因此索引或所有索引和工作树对于每个克隆都是私有的。您无法直接控制任何其他存储库的索引和工作树:您必须将其留给可能在另一台机器上操作 Git 的人(假设另一个克隆在另一台机器上)。
关于.gitignore
.gitignore 文件命名错误。更好的名字是.git-do-not-complain-about-these-files-if-they-are-untracked-and-if-they-are-untracked-and-I-use-an-en-masse-add-command-do-not-add-them-to-the-index-either。
当我们运行 git status 时,Git 抱怨 未跟踪的文件。它变得非常发牢骚!这很烦人,因为工作树是一个普通的目录,而我们使用的软件种类,我们运行的程序会在我们的工作树中创建大量构建工件。这会留下大量未跟踪的文件。 git status 命令变得嘈杂,我们的工作效率直线下降。
要让git status关闭____,我们可以在.gitignore 文件中列出这些预期的构建产品。 这对这些文件现在是否在索引中没有影响。但是如果它们不在索引中——如果它们未被跟踪现在——那么git status不会抱怨他们。
当然,如果git status 没有抱怨,那如果git add . 也“正常工作”,不添加它们,那就太好了。这就是在.gitignore 中列出文件的第二个主要效果:如果文件没有已经被跟踪——如果它不在索引中现在——我们运行@ 987654418@,我们希望 Git不要添加它。
如果文件已经在索引中(被跟踪),在.gitignore 中列出它对git status 和git add 没有影响:将检查文件的状态,并且git add 将添加文件。5 所以对于已经跟踪的文件,.gitignore 没有帮助。这就是文件名不正确的原因。但更正确的名称将无法使用,所以.gitignore 是。
在.gitignore 中列出文件还有一个副作用:它授予Git 权限clobber 文件。这主要涉及在文件未被跟踪和忽略时检查 确实 包含该文件的旧提交。结帐继续进行,现在您已将 已跟踪 文件取出,未跟踪者的数据已被彻底销毁。所以real的全名可能是.git-about-some-files-that-may-be-untracked-and-what-to-do-if-they-are:do-not-complain-and-do-not-auto-add-but-do-feel-free-to-destroy-these-files,或者什么的。 (但在许多 Microsoft 系统上不允许使用冒号字符。)
5有一些索引标志——索引的 cache 方面的一部分——可以被滥用来阻止git status 和git add添加。这些是假设不变和跳过工作树标志。它们不是为此目的而设计的,因此是上面的滥用概念,它们对解决这个问题的特定问题没有帮助,但值得一提。
你必须做什么
您有多种选择。最激烈,但最容易实施,也许最容易做到的是:创建一个全新的 Git 存储库。小心从不添加这些文件,以便它们从不被跟踪,因此永远不会成为问题。将所有系统移至新的 Git 存储库,放弃(并最终销毁)旧的 Git 存储库。
或者,您可以进行最小更新:进行新提交,与旧提交相比,删除文件。然后,转到每个部署并手动更新这些系统,小心地保存文件,同时使它们不被跟踪。您可以使用git rm --cached 技巧,或者在结帐期间将文件保存在工作树之外,或者您喜欢的任何其他内容。这些方法中的任何一种都有效。然后要非常小心,永远不要返回那些会使这些文件被跟踪的有毒提交。
在这两个选项之间,您可以使用历史重写工具(filter-branch、filter-repo、BFG,无论您喜欢什么)来获取您现有的提交并将它们转换为新的和改进的提交,其中那些文件从未提交过。这很像第一个和/或第三个选项:您仍然必须小心地进行每个部署并更新它,因为重写的存储库实际上是一个 new 存储库。它的缺点是,如果历史记录同步,拥有旧(重写前)存储库的人很容易意外地重新引入错误提交。 (他们是否这样做取决于前几次提交中的内容和/或您如何重写历史记录。)
如果您可以完全控制软件,最佳选项通常是这样的:
- 将可以/应该受版本控制的重要数据(如果有)移至新文件名。例如,这可能是
config.defaults。
- 将不得受版本控制的重要数据(因为每个站点不同)移动到新文件名。例如,这可能是
config.site。
- 确保
config.site 不会出现在任何过去、现在或将来的提交中。 请在.gitignore 中列出它,这样它就不会被意外添加和提交。
- 更新所有安装以具有正确的(根据定义,未跟踪)
config.site 文件。
- 分发新版本。现在所有站点都使用默认值和每个站点的配置。旧的提交都不需要更改。
你无法改变过去,但没有必要这样做。