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

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

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

101307 次浏览

总结

默认情况下,git pull创建合并提交,这会给代码历史记录增加噪音和复杂性。此外,pull使您很容易不考虑传入更改如何影响您的更改。

git pull命令是安全的,只要它只执行快进合并。如果git pull被配置为只做快进合并,并且当快进合并不可能时,Git将报错退出。这将使您有机会研究传入的提交,考虑它们如何影响本地提交,并决定最佳的行动方案(合并、重基、重置等)。

使用Git 2.0或更新版本,您可以运行:

git config --global pull.ff only

将默认行为更改为仅快进。Git版本介于1.6.6和1.9之间。X你必须养成打字的习惯:

git pull --ff-only

然而,对于所有版本的Git,我建议像这样配置git up别名:

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 fetchgit 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创建的merge并应用stash。

git remote update -p(或git fetch --all -p)不会修改工作目录或索引,因此在任何时候运行都是安全的,即使你有阶段性和/或非阶段性的更改。您可以暂停正在做的事情并查看其他人的提交,而不必担心存储或完成您正在处理的提交。git pull没有给你这样的灵活性。

重新基于一个远程分支

一个常见的Git使用模式是执行git pull来引入最新的更改,然后执行git rebase @{u}来消除git pull引入的合并提交。Git有一些配置选项,通过告诉git pull执行重基而不是合并,将这两个步骤简化为一个步骤,这是很常见的(参见branch.<branch>.rebasebranch.autosetuprebasepull.rebase选项)。

不幸的是,如果你有一个想要保留的未推送的合并提交(例如,一个将推送的特性分支合并到master的提交),那么在rebase之后的合并拉取(git pullbranch.<branch>.rebase设置为true)和合并拉取(默认的git pull行为)都不会起作用。这是因为git rebase在没有--preserve-merges选项的情况下消除了合并(它线性化DAG)。rebase-pull操作不能配置为保存合并,并且在合并-pull之后执行git rebase -p @{u}不会消除合并-pull引起的合并。Git v1.8.5添加了git pull --rebase=preservegit config pull.rebase preserve。这些会导致git pull在获取上游提交后执行git pull1。(感谢git pull3的提醒!)

清理已删除的分支

git pull不会修剪远程跟踪分支对应于从远程存储库中删除的分支。例如,如果有人从远程回购中删除分支foo,你仍然会看到origin/foo

这会导致用户意外地复活被杀死的分支,因为他们认为它们仍然活跃。

一个更好的选择:使用git up而不是git pull

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

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别名的替代:

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

这个版本的git up与之前的git up别名具有相同的行为,除了:

  • 如果您的本地分支没有配置上游分支,则错误消息会更加神秘
  • 它依赖于一个未被记录的特性(-p参数,传递给fetch),这个特性在Git的未来版本中可能会改变

如果您正在运行Git 2.0或更新版本

在Git 2.0和更新版本中,你可以配置git pull默认只做快进合并:

git config --global pull.ff only

这导致git pullgit pull --ff-only一样工作,但它仍然没有获取所有上游提交或清除旧的origin/*分支,因此我仍然倾向于git up

我的回答,摘自HackerNews上出现的讨论:

我很想用贝特里奇标题定律来回答这个问题:为什么git pull被认为是有害的?它不是。

  • 非线性本质上并不是坏事。如果它们代表了真实的历史,那就没问题。
  • 意外重新引入变基 upstream是错误地重写upstream历史的结果。当历史在几次回购中被复制时,你不能重写历史。
  • 修改工作目录是预期的结果;有争议的有用性,即面对hg/monotone/darcs/ other_dvcs_ing_git的行为,但本质上也不是坏的。
  • 在合并中需要暂停检查其他人的工作,这也是git pull的预期行为。如果你不想合并,你应该使用git fetch。同样,与以前流行的dvcs相比,这是git的一个特性,但这是预期的行为,本质上并不坏。
  • 让基于远程分支的重基变得困难是好的。除非万不得已,否则不要重写历史。我无论如何也无法理解这种对(虚假的)线性历史的追求
  • 不清理树枝是好的。每个回购方都知道自己想持有什么。Git没有主从关系的概念。

如果正确地使用Git,那么它不会被认为是有害的。鉴于您的用例,我知道它是如何对您产生负面影响的,但是您可以通过不修改共享历史来避免问题。

已接受的答案声明

重构基拉操作不能配置为保存合并

但是对于于Git 1.8.5,它延迟了答案的日期,你可以这样做

git pull --rebase=preserve

git config --global pull.rebase preserve

git config branch.<name>.rebase preserve

文档表示

preserve,也将--preserve-merges传递给'git rebase'时,本地提交的合并提交将不会通过运行'git pull'而被扁平。

前面的讨论有更详细的信息和图表:Git pull -rebase -preserve-merge。它还解释了为什么git pull --rebase=preservegit pull --rebase --preserve-merges不一样,后者没有做正确的事情。

前面的另一个讨论解释了rebase的保存-合并变体的实际功能,以及它如何比常规rebase: git's &;rebase -preserve-merges&;做(为什么?)复杂得多

如果你去旧的git仓库git起来,他们建议的别名是不同的。 https://github.com/aanand/git-up < / p >

git config --global alias.up 'pull --rebase --autostash'

这对我来说非常合适。