在什么情况下`git pull`是有害的?

In what cases could `git pull` be harmful?

我有一个同事声称git pull是有害的,每当有人使用它,他都会感到不安。

git pull命令似乎是更新本地存储库的规范方法。使用git pull会产生问题吗?它会产生什么问题?有没有更好的方法来更新Git存储库?


总结

默认情况下,git pull创建合并提交,这会增加代码历史记录的噪声和复杂性。此外,pull使您很容易不去想您的更改可能会受到传入更改的影响。好的。

只要只执行快进合并,git pull命令是安全的。如果将git pull配置为仅进行快进合并,并且当无法进行快进合并时,则git将退出并出现错误。这将使您有机会研究传入提交,思考它们可能如何影响您的本地提交,并决定最佳的操作过程(合并、重新平衡、重置等)。好的。

使用Git 2.0及更高版本,您可以运行:好的。

1
git config --global pull.ff only

将默认行为更改为仅快进。在Git版本介于1.6.6和1.9.x之间时,您必须养成输入的习惯:好的。

1
git pull --ff-only

但是,对于所有版本的Git,我建议配置这样的git up别名:好的。

1
git config --global alias.up '!git remote update -p; git merge --ff-only @{u}'

git up代替git pull。我喜欢这个别名而不是git pull --ff-only,因为:好的。

  • 它适用于所有(非古代)版本的git,
  • 它获取所有上游分支(不只是当前正在处理的分支),并且
  • 它清除了上游不再存在的旧origin/*分支。

git pull的问题

如果使用得当,git pull也不错。最近对git的几项更改使正确使用git pull变得更容易,但不幸的是,普通git pull的默认行为有几个问题:好的。

  • 它引入了历史上不必要的非线性
  • 这使得意外地重新引入有意在上游重新平衡的提交变得容易。
  • 它以不可预知的方式修改您的工作目录
  • 暂停你正在做的事情来回顾别人的工作对git pull很恼火。
  • 这使得很难正确地重新定位到远程分支上
  • 它不会清除远程回购中删除的分支

下面将更详细地描述这些问题。好的。非线性历史

默认情况下,git pull命令相当于运行git fetch,然后运行git merge @{u}。如果本地存储库中有未执行的提交,那么git pull的合并部分将创建合并提交。好的。

合并提交本身没有什么不好的地方,但它们可能很危险,应该尊重:好的。

  • 合并提交本身就很难检查。要理解合并是在做什么,你必须理解对所有父母的不同。传统的差异并不能很好地传递这种多维信息。相比之下,一系列正常提交很容易审查。
  • 合并冲突解决是一个棘手的问题,并且由于合并提交很难审查,错误往往会在很长一段时间内未被发现。
  • 合并可以悄悄地取代常规提交的效果。代码不再是增量提交的总和,导致对实际更改内容的误解。
  • 合并提交可能会中断一些连续的集成方案(例如,根据假设的约定,第二个父路径指向正在进行的未完成的工作,仅自动构建第一个父路径)。

当然,合并有一个时间和地点,但是理解何时应该使用合并,何时不应该使用合并可以提高存储库的实用性。好的。

请注意,Git的目的是使共享和使用代码库的演进变得容易,而不是在代码库展开时精确地记录历史。(如果您不同意,请考虑rebase命令及其创建原因。)由git pull创建的合并提交不会向其他人传递有用的语义,他们只是说,在完成更改之前,其他人碰巧推送到了存储库。如果这些合并承诺对其他人没有意义,并且可能很危险,那么为什么还要它们呢?好的。

可以将git pull配置为重新平衡而不是合并,但这也存在问题(稍后讨论)。相反,应将git pull配置为只进行快速向前合并。好的。重新引入重新平衡的承诺

假设有人调整树枝的平衡,然后用力推动它。这通常不应该发生,但有时是必要的(例如,删除一个意外提交和推送的50gib日志文件)。git pull完成的合并将把上游分支的新版本合并到仍然存在于本地存储库中的旧版本中。如果你推动结果,音叉和火把将开始你的方式。好的。

一些人可能认为真正的问题是强制更新。是的,一般来说,尽量避免用力,但有时不可避免。开发人员必须准备好处理强制更新,因为它们有时会发生。这意味着不要盲目地通过普通的git pull合并旧的承诺。好的。意外修改工作目录

git pull完成之前,无法预测工作目录或索引的外观。在执行其他操作之前,可能需要解决合并冲突,它可能会在工作目录中引入一个50gib日志文件,因为有人意外地推了它,它可能会重命名正在工作的目录,等等。好的。

git remote update -p(或git fetch --all -p允许您在决定合并或重新平衡之前查看其他人的承诺,允许您在采取行动之前制定计划。好的。难以审查他人的承诺

假设您正在进行一些更改,而其他人希望您审阅他们刚刚推送的一些提交。git pull的merge(或rebase)操作修改工作目录和索引,这意味着工作目录和索引必须是干净的。好的。

你可以先用git stash,然后用git pull,但复习完后你会怎么做?要回到原来的位置,您必须撤消git pull创建的合并并应用stash。好的。

git remote update -pgit fetch --all -p不修改工作目录或索引,因此可以随时安全运行,即使您已经进行了阶段性和/或未进行阶段性更改。你可以暂停你正在做的事情,回顾别人的承诺,而不用担心你正在做的事情被藏起来或完成。git pull并没有给你这种灵活性。好的。重新定位到远程分支

一种常见的Git使用模式是执行git pull以引入最新的更改,然后引入git rebase @{u},以消除git pull引入的合并承诺。Git有一些配置选项,通过告诉git pull执行一个rebase而不是合并(见branch..rebasebranch.autosetuprebasepull.rebase选项),可以将这两个步骤简化为一个步骤,这已经很常见了。好的。

不幸的是,如果您有一个想要保留的未执行的合并提交(例如,一个提交将一个推送的功能分支合并到master,那么既不需要设置为true的redase pull(git pull)也不需要设置为true的合并pull(默认的git pull行为),然后再设置一个rebase。这是因为git rebase消除了没有--preserve-merges选项的合并(它将DAG线性化)。不能将rebase pull操作配置为保留合并,后面跟着git rebase -p @{u}的合并pull不会消除合并pull导致的合并。更新:git v1.8.5增加了git pull --rebase=preservegit config pull.rebase preserve。这导致git pull在获取上游提交之后执行git rebase --preserve-merges。(多亏了Funkaster的支持!)好的。清除已删除的分支

git pull不会删除与从远程存储库中删除的分支相对应的远程跟踪分支。例如,如果有人从远程回购中删除分支foo,您仍然可以看到origin/foo。好的。

这会导致用户意外地恢复被杀死的分支,因为他们认为它们仍然处于活动状态。好的。一个更好的选择:用git up代替git pull

我建议创建并使用以下git up别名,而不是git pull:好的。

1
git config --global alias.up '!git remote update -p; git merge --ff-only @{u}'

此别名下载所有上游分支(修剪死分支)的所有最新提交,并尝试将本地分支快速转发到上游分支上的最新提交。如果成功,那么就没有本地提交,因此没有合并冲突的风险。如果存在本地(未完成)提交,那么快进将失败,这将给您一个在采取行动之前审查上游提交的机会。好的。

这仍然会以不可预知的方式修改您的工作目录,但前提是您没有任何本地更改。与git pull不同,git up永远不会让你看到希望你解决合并冲突的提示。好的。另一种选择:git pull --ff-only --all -p

以下是上述git up别名的替代方案:好的。

1
git config --global alias.up 'pull --ff-only --all -p'

此版本的git up与以前的git up别名具有相同的行为,除了:好的。

  • 如果您的本地分支没有配置上游分支,那么错误消息就更加神秘了。
  • 它依赖于一个未记录的特性(-p参数,传递给fetch参数),该参数可能在将来的git版本中发生变化。

如果运行的是Git2.0或更高版本

使用git 2.0及更高版本,您可以将git pull配置为默认情况下只进行快进合并:好的。

1
git config --global pull.ff only

这使得git pull的行为与git pull --ff-only的行为相似,但它仍然无法获取所有上游承诺或清除旧的origin/*分支,所以我还是更喜欢git up。好的。好啊。


从黑客新闻上的讨论中得出我的答案:

我很想用标题的更好法则来回答这个问题:为什么git pull被认为是有害的?不是的。

  • 非线性本身并不坏。如果它们代表了实际的历史,那么它们是可以的。
  • 意外地重新引入上游重新平衡的提交是错误地重写上游历史的结果。当沿着多个repo复制历史记录时,不能重写历史记录。
  • 修改工作目录是一个有争议的有用性的预期结果,即面对hg/donole/darcs/other_-dvcs_-predating_-git的行为,但又不是本质上的坏。
  • 合并需要暂停以检查其他人的工作,这也是git pull的预期行为。如果不想合并,应该使用git fetch。同样,与以前流行的DVC相比,这是Git的一个特性,但它是预期行为,而不是本质上的不良行为。
  • 使远程分支的重新平衡变得困难是很好的。除非你绝对需要,否则不要重写历史。我一辈子都无法理解这种对(假的)线性历史的追求
  • 不清理树枝是好事。每个回购都知道它想要持有什么。Git没有主从关系的概念。


如果您正确使用git,则不会被认为是有害的。考虑到您的用例,我看到了它是如何对您产生负面影响的,但是您可以通过不修改共享历史来避免问题。


接受的答复要求

The rebase-pull operation can't be configured to preserve merges

但是从1.8.5开始,你可以这样做

1
git pull --rebase=preserve

1
git config --global pull.rebase preserve

1
git config branch.<name>.rebase preserve

医生说

When preserve, also pass --preserve-merges along to 'git rebase' so that locally committed merge commits will not be flattened by running 'git pull'.

前面的讨论有更详细的信息和图表:git pull--rebase--preserve合并。这也解释了为什么git pull --rebase=preservegit pull --rebase --preserve-merges不一样,因为后者做得不对。

前面的另一个讨论解释了rebase的preserve merges变体的实际功能,以及它如何比常规的rebase复杂得多:git的"rebase--preserve merges"到底是做什么的(以及为什么?)