我想要一个可以做[Git 做不到的事情] 的解决方案
抱歉,但答案是:不,Git 不能那样做。 你可以靠近,但这并不有趣:它需要每个跑步者的努力git clone
,从那时起,反复遭遇可能导致烧伤。这就是为什么标准方法是这个答案中推荐的方法我可以“git commit”一个文件并忽略它的内容更改吗?
这可能有助于理解为什么Git 不能这样做。让我们更具体地看一下“那个”是什么:
- 将整个
~/.cfg/
(包括~/.cfg/local/
)的初始上传保存在在线存储库中。
这个,你可以的。但是措辞很奇怪,因为 Git 不存储文件。Git 存储commits,其中包含文件。这可能看起来只是语义,但话又说回来,它是关于“热水”水是否适合淋浴(40˚C / 104˚F:热,但不会烫伤),或者会给你第二次 -度烧伤(95˚C / 203˚F:在标准压力下接近沸腾)。
因此,您可以有一个包含文件的提交,包括cfg/foo
和cfg/local/bar
. 到目前为止,没有真正的问题——主要问题是你不能有一个包含空目录的提交cfg/local/
,因为 Git 在每个提交中只存储文件本身,而不是包含目录:它假定以后使用存储库的任何人都将创建目录自动根据需要,只要有要存储的文件,其名称会强制将来/其他 Git 调用os.mkdir
或创建包含该文件的目录的任何内容。
- 每当我做标准时推送内容
~/.cfg/
而不是内容~/.cfg/local/
git add -A; git commit -m "asdf"; git push
这是第一个问题,那些“纯粹的”语义至少有点烫伤:Git 不推送文件。Git 推送提交。
这里有三个命令。第一个git add -A
,告诉 Git:更新索引中记录的所有文件的索引副本,方法是用我的工作树中的新版本替换它。 第二个git commit
,告诉 Git:使用存储在索引中的文件进行新的提交。 第三个,git push
告诉 Git:向其他 Git 发送一些提交,然后要求其他 Git 设置一个或多个它的引用,例如它的 - 它refs/heads/master
的master
分支 - 到某个哈希 ID。
这带来了这个新术语index,这就是麻烦开始的地方。
如果您的cfg/local/bar
文件在您的索引中,它将在您的提交中。如果它不在您的索引中,则它不会在您的提交中。这很简单,但它的含义很糟糕:
您可以在不触及工作树版本 ( ) 的情况下从索引中删除git rm --cached cfg/local/bar
文件,但这会导致未来出现问题。
或者,您可以在索引中的文件副本上设置--assume-unchanged
或位。这几乎足够好,但还不够。(顺便说一句,这两者或多或少是等价的,但“skip worktree”是用于这种用途的——除了它的真正意图是用于稀疏结帐。我将在下面写“skip worktree”但是这实际上意味着任何一个。)--skip-worktree
设置该位要求您在git clone
. 该索引对您的存储库副本是私有的,因此运行的每个人也git clone
必须运行此git update-index
命令,至少一次,在git clone
. (Git 不会让您通过 Git 本身自动执行此操作,当然您可以编写一个脚本来执行此操作并分发该脚本。)
正如您可能已经看到的那样,这只几乎有效。
- 拉的内容
~/.cfg/
但不是~/.cfg/local/
我时的内容git pull
再一次,Git 会在这里烧死你。问题在于它git pull
本身并不是真正的事情:它意味着run git fetch
,然后运行第二个 Git 命令,而第二个Git 命令会造成麻烦。
第二个 Git 命令通常是git merge
,我们现在假设它是。另一个选项对您git rebase
来说更糟,因为 rebase 本质上是重复git cherry-pick
的,每个cherry-pick 操作本身就是一个合并,从而导致多个合并。
像提交一样,合并发生在索引中或通过索引发生。Git 将来自三个提交的所有文件加载到索引中,在两个单独的步骤中配对文件(基础与“我们的”,以及基础与他们的),然后组合配对。因此,这会合并索引中的每个文件,或者,如果先前提交中索引中的文件现在不在索引中,则删除或重命名文件。
这意味着如果一个文件cfg/local/bar
存在于合并基础提交和“他们的”提交中——并且它需要在那里,如果你想要git clone
填充cfg/local
初始值cfg/local/bar
——那么它也需要存在于“我们的”提交中,否则 Git 会坚持删除它以保留我们的更改。反过来,这意味着如果他们在提交中更改了副本,Git 也会希望将他们的更改应用于您的提交中的副本。
如果您曾经git update-index
对--skip-worktree
标志大惊小怪,那么您一直在重新提交原始版本cfg/local/bar
。flag 只是告诉 Git:嘿,不要看我自己版本的这个文件,只是假设索引中的副本仍然正确。 这会影响git add -A
步骤:而不是更新索引中列出的所有文件,它实际上是:更新所有没有特别标记的文件。 您可以随心所欲地更改cfg/local/bar
,并且git add -A
会跳过更新:它不会将您的工作树复制cfg/local/bar
回索引中,而是保留在您第一次为您git clone
运行时将其放入索引中的副本git checkout
。
因此,您的所有提交都有一个cfg/local/bar
,但这些提交存储在其中的内容,在每个提交中,与您运行时获得的内容相同,即使您更改了工作树副本。您的 skip-worktree 位告诉您的 Git 只保留索引副本,它已经完成了。cfg/local/bar
git clone
cfg/local/bar
但是现在是合并时间,并且无论出于何种原因,他们已经改变了它们 cfg/local/bar
——原因并不重要,重要的是他们确实改变了它——<em>现在你的 Git 面临着合并你的更改的工作(无)随着他们的变化(一些)。它通过进行唯一的更改来做到这一点——当然是他们的——现在你的 Git 会坚持将更新的内容复制cfg/local/bar
到你的工作树中。这将覆盖您的cfg/local/bar
,这就是痛点:这就是这种方法烧伤您的地方。
如果他们 从来没有(从来没有,一次都没有)改变他们的cfg/local/bar
,这种方法——设置 skip-worktree——实际上会起作用。但这取决于陌生人的好意,或者至少取决于每次提交中的本地配置都cfg/local/bar
完全相同的想法……在这种情况下,提交它的意义何在?
但是,如果他们确实更改了它,那么当您将他们的更改与您的缺少更改合并时,您会被烧毁(轻度或其他),因为 Git 会希望cfg/local/bar
用他们更新的内容覆盖您。
另一种方法是在早期从索引中删除你的,更糟糕的是:现在你推送的每个提交都没有文件。Git 将此视为一个命令:当从有文件的提交转到没有文件的提交时,删除文件。 因此,如果您采用这种方法,您就是更改文件的人!你告诉其他人:删除这个文件!cfg/local/bar
唯一真正、100% 保证、正确的处理方法是:从一开始就不要提交文件。 如果存储库中的每个提交都没有cfg/local/bar
,则该文件将永远不会放入索引中。如果该名称也列在 a.gitignore
中,则不会自动“添加所有文件”将其添加到索引中,因此不会出现在以后的提交中。这意味着当你开始时它不会在那里,当你完成时也不会。Git 永远不会想要合并它,也不会覆盖它的副本。它始终是一个未被跟踪和忽略的文件,存在于您的工作树中,但不存在于您的任何提交中。
当然,这意味着有一点最初的痛苦:每次你跑步的时候你git clone <url>
还必须做:cp -r .cfg/local-committed/ .cfg/local
。但是如果你要使用--skip-worktree
的话,那么每次你跑的时候git clone <url>
你必须马上跟着那个git update-index --skip-worktree .cfg/local/bar
。所以它与糟糕的选择完全一样的痛苦,没有任何坏处。
此外,如果您控制该软件,您可以设置该软件,如果您第一次运行该程序.cfg/local/
时不存在该软件,则该程序通过从. 然后“第一次设置”的痛苦也消失了! 这就是为什么将默认配置提交到一个单独的文件中,用户手动或自动复制到本地配置文件,该文件永远是一个未跟踪的文件,是正确的解决方案。 .cfg/local/
.cfg/local-committed/