首页 > 解决方案 > 从A切换到B时,如何将工作树跟踪的文件保留在分支A中但在分支B中被忽略?

问题描述

我有一个基于 git 的部署设置,其中生产机器有一个“部署”分支的克隆,我使用该部署分支来跟踪不应由开发分支跟踪的配置文件,如凭据文件。我需要在本地使用开发分支中的一些未跟踪的配置文件,但是当从部署分支切换时,git 不会将这些文件保留在工作树中。为了能够使用这些文件,我需要做

git checkout deploy configuration/file
git reset HEAD configuration/file

如何让 git 将未跟踪的文件保留在工作树中?

要重现此问题,您可以执行

git init test-repo
cd test-repo
git checkout -b A
echo 'null' > A_file
git add A_file
git commit -m "Make branch A"
git checkout -b B
echo 'null' > B_file
git add B_file
git commit -m "Make branch B"

现在你在 branch B,切换到 branch A ( git checkout A) 你会看到它B_file不在工作树中。

标签: git

解决方案


简短的回答是:甚至不要尝试这样做。 你不能让 Git 做你想做的事。是答案的原因有点长,并且包括一些技术定义。

首先,我们应该注意到 Git 并不是关于分支(或至少是分支名称)。Git 真的是关于提交。提交是从一个 Git 存储库共享到另一个存储库的内容。该git push操作将提交从您的 Git 发送到另一个 Git。分支名称仅用于帮助我们(和 Git)定位提交;发送一些提交的git push操作以请求让另一个Git记住该提交结束,通常通过另一个 Git 中的分支名称。1 因此,在深入了解 Git 的索引和您的工作树之前,让我们先看一下提交。


1这意味着,除其他外,您实际上不需要在 Git 中使用分支名称,但需要在其他Git 中使用名称。没有理由这样做,而且在分离的 HEAD 上工作通常是个坏主意,但在技术上是可行的


提交

记住这些关于提交的事情:

  • 每一个都用一个大而难看的哈希 ID 编号。
  • 这些数字看起来是随机的,但不是。每个实际上都是内部提交对象的完整内容的加密校验和。所有 Git 都以相同的方式计算这些,这就是我们如何让两个 Git 同意提交获得其特定编号的方式。
  • 每个提交包含两个部分。首先是主要数据,它是 Git 在您提交时知道的所有文件的快照。稍后我们将专注于该部分。然后是元数据,包含提交作者的姓名和电子邮件地址等内容。元数据还包含先前或提交的哈希 ID,或者对于合并提交,两个或多个先前提交。
  • 由于哈希 ID,任何提交的内容都会一直冻结:没有人可以更改任何内容。
  • 为了降低所有这些快照的空间使用率,并且由于提交中的文件被冻结,Git 对文件进行了重复数据删除。

所以这意味着一个主要重用来自其他提交的文件的提交几乎不需要空间:只需要一点用于作者和提交者的数据,加上可能是这个特定提交独有的一些对象。但这也意味着每次提交中的文件对于完成任何新工作都是无用的。

提交内容、Git 的索引和你的工作树

一般来说,版本控制系统都倾向于共享这种“冻结的提交,可用的工作”模式,而这里 Git 并没有什么不同:当你签出(或切换到)提交时——Git 通过其哈希 ID 执行此操作,通常使用用于查找哈希 ID 的分支名称——Git 将非 Git 程序无法读取且无法更改的冻结格式文件复制到普通的日常读/写文件中。因此,您将获得每个“活动”或当前提交文件的两个副本。有用的是 Git 称之为工作树或工作树的地方。

不过,在这里,Git 与大多数 VCS 不同:Git 不是每个文件只有这两个必要的副本,而是添加了第三个副本。这第三个副本是冻结格式,并且已经删除了重复数据,因此无论如何它通常都不是副本。这第三个“副本”位于 Git 所称的不同地方,即indexstaging area,或者(现在很少见)cache

索引有多个角色,但我们在这里关心的主要是它如何用于进行新的提交。这是它的暂存区作用。当您git checkout进行一些提交时,Git 会从您选择的提交中填充其索引。所以现在索引保存该提交中的每个文件——而且只有那些文件。实际上,这些是 Git 知道的文件。

Git 还将这些文件复制到您的工作树中,将它们扩展为有用的形式。这意味着在从新克隆的存储库进行初始签出时,例如,您现在拥有每个文件的三个副本,来自您选择的任何分支上的最后一个提交。它们三个都匹配,其中两个——提交副本和索引副本——实际上共享底层存储(可能还有许多其他提交)。

切换提交

由于索引以去重(按内部哈希 ID 编号)形式存储每个文件,就像每个提交一样,Git 立即知道,当您从一个提交切换到另一个提交时,两个提交中的哪些文件是相同的,哪些是相同的是不同的,索引中的哪些条目可能需要更新。这使 Git 能够仅删除和/或替换那些需要删除和替换的文件。因此,从提交切换a123456到提交fedcba9可以非常快,并且几乎不会干扰您的工作树,如果这些提交相似的话。

构建一个新的提交

当你运行时git commit,Git 将构建一个新的提交。这个新的提交需要一个快照。快照由 Git 索引中的任何内容提供

这些文件——Git 索引中的文件——本质上是提议的下一次提交。它们是 Git 知道的文件。它们已经处于冻结和去重的格式,准备好进行新的提交:几乎没有任何实际工作要做来进行新的提交。Git 只需要将索引转换为内部 Git树对象,并使用具有正确元数据的提交包装该树对象。

新提交的父级将是当前提交。您将成为作者和提交者——此时 Git 需要您的姓名和电子邮件地址。元数据中的日期和时间戳将是“现在”,写出此数据的行为将为您的新提交提供新的随机散列 ID。

生成此提交后,Git 现在将其哈希 ID 写入当前分支名称,以便您的分支名称现在标识此分支上的新最终提交。如果多个分支名称标识了最后一个提交那么您之前签出的那个提交,那么,现在您在您的名称git checkout中使用的名称标识了这个新的提交。其他名称仍然标识旧提交,即现在这个新提交的父级。

跟踪的文件、未跟踪的文件和.gitignore

这也让我们了解了跟踪文件未跟踪文件的定义。鉴于你的工作树是的,你可以用它做任何你喜欢的事情,包括创建 Git 不知道的文件。这些文件不在 Git 的索引中,这使得它们无法跟踪。

就是这样:一个未跟踪的文件只是现在在你的工作树中的任何文件并且现在不在Git 的索引中。鉴于您可以随意创建和删除工作树文件,您可以随时创建新的未跟踪文件,并随时删除它们。

给定git addand git rm --cached,您也可以随时将文件放入 Git 的索引中,或随时从 Git 的索引中删除文件。请注意,这git add意味着使该命名文件的索引副本与工作树副本匹配:如果您删除文件F的工作树副本然后运行,Git 也会删除F的索引副本。但是可以让您在不触及工作树副本的情况下删除索引副本。git add Fgit rm --cached

.gitignore文件并不是真正关于忽略文件。如果你运行git commit,Git 将做出一个新的提交,其中包含 Git 索引中的那些文件。如果您没有将工作树文件放入 Git 的索引中,或者工作树副本和索引副本不匹配,Git 只会使用索引中的任何内容。所以没有必要明确地忽略任何东西:这只是索引中有什么的问题。如果该文件索引中,它将被提交,即使它在.gitignore.

那么.gitignore,真正要做的是告诉 Git:

  1. 不要抱怨某些文件在工作树中,但不在索引中。通常git status会为此抱怨。

  2. 当使用git add更新或添加许多文件的整体时,如果它还没有,请跳过它。

该文件可能应该命名为.git-do-not-complain-about-these-files-and-do-not-auto-add-them-with-en-masse-add-operations,或类似的名称;但这个名字很荒谬,.gitignore尽管语义不匹配,但它确实如此。

为什么这让你很难做你想做的事

你有两个提交,我们可能想要调用AB,两个分支名称标识这两个提交。您在这里使用了名称AB分支名称,但我将它们重命名为branch-Aand branch-B,并绘制如下提交:

  B   <-- branch-B
 /
A   <-- branch-A

提交A包含一个名为A_file. CommitB包含两个文件,分别命名为A_fileB_file.

签出 commit A,无论你怎么做——通过哈希 ID、 using branch-A、 usingbranch-B~1或其他方式——都会导致在 Git 索引中的副本,A_file而不是在 Git 索引中的副本B_file。如果B_file Git 的索引中,Git 将删除它删除工作树副本。

签出 commit B,但是你这样做——它现在只能通过名称branch-B或其原始哈希 ID 找到——导致在 Git 索引中的A_file副本和在 Git 索引中的副本B_file。这也会用这些文件填充您的工作树。

任何忽略文件中的内容都无关紧要,无论是.gitignore还是.git/info/exclude其他任何地方:这些是提交快照中的文件,因此无论何时切换到这些提交,都必须将它们复制到 Git 的索引中。这样,如果您在 上进行新提交branch-A,它将具有正确的文件集:

git checkout branch-A   # updates index to hold A_file and lack B_file
# do whatever work you do here
git commit              # makes new commit C from whatever is in Git's index

要将新的提交发送到其他 Git,您必须首先进行新的提交。然后,您可以使用git push将该提交及其包含的任何快照发送到另一个 Git,并要求另一个 Git 适当地更新分支名称。

那么你应该怎么做?

不幸的是,这个问题没有真正正确的答案。

通常的标准技巧是将配置信息完全保留在 Git 存储库之外。这通常是一个好主意,因为存储库可能广泛可用,并且您不希望在广泛可用的数据中包含秘密或至少有意未发布的密钥等配置项。但是保留一个包含配置数据的存储库通常很好,原因与我们将源代码保存在存储库中的原因相同。

一种可行的混合方式是将配置保存在单独的存储库中。这个单独的存储库可以与源存储库完全断开,或者您可以使用子模块(或原始 gitlinks)将特定项目绑定在一起。这确实会要求配置文件位于项目的工作树之外,这通常是个好主意。

另一种选择是在项目中保留示例默认配置。默认配置可能非常有用:现在实用且有用的配置非常小,仅包含那些不是默认设置的设置。如果项目更改并获得更多和/或不同的默认值,运行配置可能根本不需要任何更改。无论您是否将部署的配置放入另一个(单独的)存储库,这都可以很好地工作。


推荐阅读