【问题标题】:Is it possible to use ".gitignore" from the remote during a pull?拉动期间是否可以从遥控器使用“.gitignore”?
【发布时间】:2021-04-16 16:40:10
【问题描述】:

我遇到了一些以前签入 Git 的文件现在需要被忽略的情况。为了忽略它们,我将文件添加到“.gitignore”并执行了以下操作:

git rm -r --cached .
git add --all
git commit -m "Removed files from git tracking that should be ignored"
git push

现在我需要将这些“.gitignore”更改拉到另一台服务器,但是当我执行git pull 时,刚刚添加到“.gitignore”的文件不会被忽略,而是会得到完全删除!

我认为正在发生的事情是在拉取期间它正在使用不会忽略这些文件的本地“.gitignore”文件......并且它检测到这些文件不再在 git 中,所以它只是将它们删除。如果我手动添加文件并再次执行 git pull 则它开始正常工作(现在正确的“.gitignore”文件在服务器上。)

有没有办法告诉git pull 使用来自远程服务器的“.gitignore”文件而不是本地文件,这样这些文件会被正确忽略并且不会在 git pull 上被删除?

【问题讨论】:

  • 无需考虑遥控器——即使在本地也无法做到这一点。试试git checkout master~ && git checkout master - 文件将被删除,因为git 看到文件已从最新提交中删除。
  • 那我该如何解决呢?当我进行拉动时,它会导致配置文件被清除,这反过来又让我的同事非常沮丧。他们并不完全理解 Git 是如何工作的(这就是为什么这些文件一开始就被错误地签入的原因),所以即使我试图解释发生了什么事情,当事情破裂时,责任仍然落在我身上。
  • 你不能。不删除文件是唯一的方法。频繁备份有助于恢复已删除的文件。
  • “不删除文件是唯一的方法”——尽管我没有删除文件,Git 正在为我删除它们。我所做的只是将它们添加到.gitignore

标签: git gitignore git-pull


【解决方案1】:

.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 分支上。提交 IJ 目前仅在 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 --stagegit 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 resetgit 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 statusgit 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 statusgit add添加。这些是假设不变和跳过工作树标志。它们不是为此目的而设计的,因此是上面的滥用概念,它们对解决这个问题的特定问题没有帮助,但值得一提。


你必须做什么

您有多种选择。最激烈,但最容易实施,也许最容易做到的是:创建一个全新的 Git 存储库。小心从不添加这些文件,以便它们从不被跟踪,因此永远不会成为问题。将所有系统移至新的 Git 存储库,放弃(并最终销毁)旧的 Git 存储库。

或者,您可以进行最小更新:进行新提交,与旧提交相比,删除文件。然后,转到每个部署并手动更新这些系统,小心地保存文件,同时使它们不被跟踪。您可以使用git rm --cached 技巧,或者在结帐期间将文件保存在工作树之外,或者您喜欢的任何其他内容。这些方法中的任何一种都有效。然后要非常小心,永远不要返回那些会使这些文件被跟踪的有毒提交。

在这两个选项之间,您可以使用历史重写工具(filter-branch、filter-repo、BFG,无论您喜欢什么)来获取您现有的提交并将它们转换为新的和改进的提交,其中那些文件从未提交过。这很像第一个和/或第三个选项:您仍然必须小心地进行每个部署并更新它,因为重写的存储库实际上是一个 new 存储库。它的缺点是,如果历史记录同步,拥有旧(重写前)存储库的人很容易意外地重新引入错误提交。 (他们是否这样做取决于前几次提交中的内容和/或您如何重写历史记录。)

如果您可以完全控制软件,最佳选项通常是这样的:

  • 将可以/应该受版本控制的重要数据(如果有)移至新文件名。例如,这可能是config.defaults
  • 不得受版本控制的重要数据(因为每个站点不同)移动到新文件名。例如,这可能是config.site
  • 确保config.site 不会出现在任何过去、现在或将来的提交中。 .gitignore 中列出它,这样它就不会被意外添加和提交。
  • 更新所有安装以具有正确的(根据定义,未跟踪)config.site 文件。
  • 分发新版本。现在所有站点都使用默认值和每个站点的配置。旧的提交都不需要更改。

你无法改变过去,但没有必要这样做。

【讨论】:

  • 我喜欢这种粗鲁的新角色。
  • 这可能是我见过的最好的答案。感谢您为此付出的所有时间和精力。我在想选项 2 是我想要使用的(一旦我们做对了,我们可能永远不会回到“中毒”提交)但我不确定我需要使用哪些命令才能“仔细更新" 每个服务器都有新的提交,而不删除现在在.gitignore 中列出的文件。我会做一个git fetch 然后以某种方式使用索引来阻止它在我签出新提交之前删除文件吗?
  • 如果是我,我会 (1) 将文件保存在某处(mkdir /tmp/save; cp &lt;files&gt; /tmp/save/ 或类似的地方)以防万一; (2) 运行git fetch; git log origin/&lt;name&gt;; git rm --cached &lt;paths&gt;; git merge --ff-only。 fetch+merge 只是一个扩展的git pull --ff-only,它允许我在两者之间插入命令:在这种情况下,git log 以确保我看到我期望看到的内容,git rm --cached 试图确保我没有'没有得到毒药效果,然后是最终的merge-for-fast-forward-checkout-effect。如果一切顺利,我可以rm -r /tmp/save,如果没有,我还有文件...
  • 好的,我找到了一个运行良好的工作流程!获取远程内容:git fetch... Checkout JUST .gitignore from the remote:git checkout origin/master .gitignore... 从索引中删除忽略的文件:git rm -r --cached .... 合并更改:git merge --ff-only origin/master... 取完这些步骤文件被更新并且被忽略的文件不会被删除!
猜你喜欢
  • 2020-08-05
  • 2023-01-22
  • 1970-01-01
  • 1970-01-01
  • 2014-08-11
  • 1970-01-01
  • 2013-01-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多