在什么情况下,“拉扯”是有害的?

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

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

概要

默认情况下, git pull会创build合并提交,这会增加代码历史logging的噪音和复杂性。 另外, pull可以很容易地不考虑你的变化可能会受到传入变化的影响。

只要执行快进合并, git pull命令是安全的。 如果git pull被configuration为只执行快进合并,而不能快进合并,那么Git将会退出并显示错误。 这将使您有机会研究传入提交,考虑如何影响您的本地提交,并确定最佳的操作方式(合并,重新分配,重置等)。

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

 git config --global pull.ff only 

将默认行为更改为仅快进。 使用1.6.6和1.9.x之间的Git版本,你将不得不习惯打字:

 git pull --ff-only 

但是,对于所有版本的Git,我build议像这样configuration一个git up别名:

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

并使用git up而不是git pull 。 我更喜欢这个别名,因为:

  • 它适用于所有(非古老的)Git版本,
  • 它获取所有上游分支(不只是你正在工作的分支),和
  • 它清除不再存在于上游的旧的origin/*分支。

git pull问题

如果使用正确的话, git pull是不错的。 最近Git的一些变化使得使用git pull更容易,但不幸的是,一个普通的git pull的默认行为有几个问题:

  • 它在历史上引入了不必要的非线性
  • 它可以很容易地意外地重新提交上游有意重新提交的提交
  • 它以不可预知的方式修改你的工作目录
  • 暂停你在做什么来检查别人的工作是令人讨厌的git pull
  • 这使得很难正确地分配到远程分支上
  • 它不清除在远程回购中删除的分支

下面将更详细地描述这些问题。

非线性历史

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

合并承诺本身并没有什么坏处,但它们可能是危险的,应该得到尊重:

  • 合并提交本质上很难检查。 要了解合并的内容,您必须了解所有父母的差异。 传统的diff不能很好地传达这个多维信息。 相比之下,一系列正常的提交很容易查看。
  • 合并冲突解决是棘手的,错误往往不被检测很长一段时间,因为合并提交很难回顾。
  • 合并可以悄悄地取代常规提交的效果。 代码不再是增量提交的总和,导致对实际改变的误解。
  • 合并提交可能会破坏一些持续的整合scheme(例如,在假定的约定下自动构build第一父path,第二父母指出进行中的不完整工作)。

当然,还有一段时间和一个合并的地方,但是理解何时合并,何时不应该被使用,可以提高你的仓库的实用性。

请注意,Git的目的是使分享和使用代码库的演变变得简单,而不是精确logging它展现的历史。 (如果您不同意,请考虑rebase命令及其创build原因。)由git pull创build的合并提交不会将有用的语义传递给其他人 – 他们只是说在别人发生变化之前,别人碰巧推送到存储库。 为什么这些合并提交,如果他们对别人没有意义,可能是危险的?

有可能configurationgit pull来代替合并,但也有问题(稍后讨论)。 相反,应该将git pullconfiguration为只执行快进合并。

重新引入退出承诺

假设有人重组了一个分支并强行推送它。 这通常不应该发生,但有时也是必要的(例如,删除一个被意外排列和推送的50GiB日志文件)。 由git pull完成的合并将把上游分支的新版本合并到本地存储库中仍然存在的旧版本中。 如果你推动的结果,高音叉和火把将开始你的方式。

有人可能会认为真正的问题是强制更新。 是的,通常build议尽可能避免推力,但有时候是不可避免的。 开发人员必须准备好处理强制更新,因为它们有时会发生。 这意味着不要通过一个普通的git pull来盲目地合并旧的承诺。

惊喜工作目录修改

git pull完成之前,没有办法预测工作目录或索引的外观。 在执行任何其他操作之前,可能会出现合并冲突,可能会在工作目录中引入一个50GiB日志文件,因为有人不小心推动了它,可能会重命名正在工作的目录等。

git remote update -p (或git fetch --all -p )允许你在决定合并或重新绑定之前查看其他人的提交,以便在采取行动之前形成一个计划。

难以回顾他人的承诺

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

你可以使用git stash然后git pull ,但是当你完成审查时你会怎么做? 为了回到你所在的位置,你必须撤消由git pull创build的合并并应用存储。

git remote update -p (或git fetch --all -p )不会修改工作目录或索引,所以即使您已经进行了暂存和/或暂停更改,也可以随时运行。 你可以暂停你正在做的事情,并检查别人的提交,而不必担心隐藏或完成正在进行的提交。 git pull不会给你那么大的灵活性。

重新绑定到远程分支上

一个常见的Git使用模式是做一个git pull来引入最新的变化,然后用git rebase @{u}来消除git pull引入的合并提交。 Git有一些configuration选项可以通过告诉git pull执行rebase而不是合并(参见branch.<branch>.rebasebranch.autosetuprebasepull.rebase选项)。

不幸的是,如果你有一个你想保留的未压缩的合并提交(例如,一个提交将一个推送的特性分支合并到master ),那么也不会有一个rebase-pull(带有branch.<branch>.rebase设置为true ) merge-pull(默认的git pull行为)后面的rebase会起作用。 这是因为git rebase消除了合并(它使DAG线性化)没有--preserve-merges选项。 无法将rebase-pull操作configuration为保留合并,而merge-pull后跟git rebase -p @{u}将不会消除merge-pull导致的合并。 更新: Git v1.8.5增加了git pull --rebase=preservegit config pull.rebase preserve 。 这些导致git pullgit rebase --preserve-merges后提取上游提交。 (感谢funkaster的单挑!)

清理删除的分支

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

这导致用户意外地复活死亡分支,因为他们认为他们仍然活跃。

更好的select:使用git up而不是git pull

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

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

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

这仍然以不可预知的方式修改您的工作目录,但前提是您没有任何本地更改。 不像git pullgit up永远不会让你提示你去修复合并冲突。

另一个选项: git pull --ff-only --all -p

以下是上述git up别名的替代方法:

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

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

  • 如果您的本地分支未configuration上游分支,则错误消息会更加隐蔽
  • 它依赖于一个未公开的特性( -p参数,传递给fetch ),可能在未来版本的Git中改变

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

使用Git 2.0和更新版本,您可以将git pullconfiguration为仅默认进行快速合并:

 git config --global pull.ff only 

这会导致git pullgit pull --ff-only ,但是它仍然不能获取所有上游提交或者清除旧的origin/*分支,所以我还是比较喜欢git up

我的回答,从HackerNews上的讨论中得出:

我很想用Goodidge标题法来回答这个问题:为什么git pull被认为是有害的? 事实并非如此。

  • 非线性不是本质上不好的。 如果他们代表实际的历史,他们就没事了。
  • 意外重新实施重新启动的上游是错误地重写上游历史的结果。 当历史沿着几个回购点复制时,您不能重写历史logging。
  • 修改工作目录是一个预期的结果; 有争议的用处,即面对hg / monotone / darcs / other_dvcs_predating_git的行为,但又不是本质上不好的。
  • 暂停审查其他人的工作是合并所需要的,这也是git pull的预期行为。 如果你不想合并,你应该使用git fetch。 再次,这是git与之前stream行的dvcs相比的一个特质,但它是预期的行为而不是内在的坏。
  • 难以对偏远分支进行重组是件好事。 除非你绝对需要,否则不要重写历史。 我不能为我的生活理解这种(假)线性历史的追求
  • 没有清理分行是好的。 每个回购知道什么想要举行。 Git没有主从关系的概念。

如果您正确使用Git,则认为这不是有害的。 我看到它是如何影响你负面的给你的用例,但你可以通过不修改共享历史logging来避免问题。

接受的答案要求

无法将重置拉取操作configuration为保留合并

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

 git pull --rebase=preserve 

要么

 git config --global pull.rebase preserve 

要么

 git config branch.<name>.rebase preserve 

文档说

preserve,也通过preserve, --preserve-merges到“git rebase”,以便本地提交的合并提交不会通过运行“git pull”而变平。

这个前面的讨论有更详细的信息和图表: git pull –rebase –preserve-merges 。 这也解释了为什么git pull --rebase=preservegit pull --rebase --preserve-merges不一样git pull --rebase --preserve-merges ,这不正确。

以前的另一个讨论解释了rebase的保留合并变体究竟是干什么的,以及它如何比常规的rebase复杂得多: git的“rebase -preserve-merges”究竟干什么(以及为什么?)