总结
默认情况下,git pull 创建合并提交,这会增加代码历史的噪音和复杂性。此外,pull 可以让您轻松不去想您的更改可能会如何受到传入更改的影响。
git pull 命令是安全的,只要它只执行快进合并。如果 git pull 被配置为只进行快进合并,并且当快进合并不可行时,Git 将退出并报错。这将使您有机会研究传入的提交,考虑它们可能如何影响您的本地提交,并决定最佳的行动方案(合并、变基、重置等)。
使用 Git 2.0 及更高版本,您可以运行:
git config --global pull.ff only
将默认行为更改为仅快进。对于 1.6.6 和 1.9.x 之间的 Git 版本,您必须养成打字的习惯:
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 fetch 后跟git merge @{u}。如果本地仓库中有未推送的提交,git pull 的合并部分会创建一个合并提交。
合并提交本身并没有什么坏处,但它们可能很危险,应该受到尊重:
- 合并提交本身就很难检查。要了解合并在做什么,您必须了解所有父母的差异。传统的差异不能很好地传达这种多维信息。相比之下,一系列正常的提交很容易审查。
- 合并冲突解决很棘手,而且由于合并提交难以审查,因此错误通常会在很长一段时间内未被发现。
- 合并可以悄悄地取代常规提交的效果。代码不再是增量提交的总和,从而导致对实际更改内容的误解。
- 合并提交可能会破坏某些持续集成方案(例如,在假定的约定下,仅自动构建第一父路径,第二父路径指向正在进行的未完成工作)。
当然,合并是有时间和地点的,但是了解何时应该和不应该使用合并可以提高存储库的实用性。
请注意,Git 的目的是让共享和使用代码库的演变变得容易,而不是准确地准确记录历史的展开。 (如果您不同意,请考虑rebase 命令及其创建原因。)git pull 创建的合并提交不会向其他人传达有用的语义——他们只是说其他人碰巧在您完成之前推送到存储库随着你的变化。如果这些合并提交对其他人没有意义并且可能很危险,为什么还要进行这些合并提交?
可以将git pull 配置为rebase 而不是merge,但这也有问题(稍后讨论)。相反,git pull 应配置为仅进行快进合并。
重新引入 Rebased-out Commits
假设有人重新设置分支并强制推送它。这通常不应该发生,但有时是必要的(例如,删除意外提交和推送的 50GiB 日志文件)。 git pull 完成的合并会将上游分支的新版本合并到本地存储库中仍然存在的旧版本中。如果你推动结果,音叉和火把就会开始向你靠近。
有些人可能会争辩说,真正的问题是强制更新。是的,通常建议尽可能避免用力推动,但有时它们是不可避免的。开发人员必须准备好应对强制更新,因为它们有时会发生。这意味着不要通过普通的git pull 盲目地合并旧的提交。
惊喜工作目录修改
在完成git pull 之前,无法预测工作目录或索引的外观。在执行其他任何操作之前,您可能必须解决合并冲突,它可能会在您的工作目录中引入一个 50GiB 的日志文件,因为有人不小心推送了它,它可能会重命名您正在工作的目录,等等。
git remote update -p(或git fetch --all -p)允许您在决定合并或变基之前查看其他人的提交,让您在采取行动之前制定计划。
难以审查其他人的提交
假设您正在进行一些更改,而其他人希望您查看他们刚刚推送的一些提交。 git pull 的合并(或变基)操作修改了工作目录和索引,这意味着你的工作目录和索引必须是干净的。
您可以使用git stash,然后使用git pull,但是当您完成审核后会做什么?要回到原来的位置,您必须撤消 git pull 创建的合并并应用存储。
git remote update -p(或git fetch --all -p)不会修改工作目录或索引,因此它可以随时安全运行——即使您已暂存和/或未暂存更改。您可以暂停正在执行的操作并查看其他人的提交,而无需担心存储或完成您正在处理的提交。 git pull 没有给你那种灵活性。
重新定位到远程分支
一个常见的 Git 使用模式是执行 git pull 以引入最新更改,然后执行 git rebase @{u} 以消除 git pull 引入的合并提交。 Git 有一些配置选项可以通过告诉git pull 执行变基而不是合并来将这两个步骤减少到一个步骤,这很常见(参见branch.<branch>.rebase、branch.autosetuprebase 和pull.rebase 选项)。
不幸的是,如果您想要保留一个未推送的合并提交(例如,将推送的功能分支合并到 master 的提交),那么 rebase-pull(git pull 和 branch.<branch>.rebase 设置为 @987654369 @) 也不是合并拉动(默认的git pull 行为),然后是变基。这是因为 git rebase 在没有 --preserve-merges 选项的情况下消除了合并(它将 DAG 线性化)。不能将 rebase-pull 操作配置为保留合并,并且后跟 git rebase -p @{u} 的合并拉动不会消除由合并拉动引起的合并。 更新: Git v1.8.5 添加了git pull --rebase=preserve 和git config pull.rebase preserve。这些导致git pull 在获取上游提交后执行git rebase --preserve-merges。 (感谢funkaster 的提醒!)
清理已删除的分支
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 pull 的行为类似于git pull --ff-only,但它仍然不会获取所有上游提交或清除旧的origin/* 分支,所以我仍然更喜欢git up。