我有一个同事声称git pull是有害的,每当有人使用它,他都会感到不安。
git pull命令似乎是更新本地存储库的规范方法。使用git pull会产生问题吗?它会产生什么问题?有没有更好的方法来更新Git存储库?
- @卡尔森:meta.stackexchange.com/questions/17463/…
- 或者您只需git pull --rebase并将此策略设置为新分支git config branch.autosetuprebase的默认策略。
- Knoopx有权这样做,将--rebase标志添加到git pull中,使本地与远程同步,然后在更新的本地之上重放本地更改。然后,当您推送您正在做的所有事情时,将您的新提交附加到远程端。很简单。
- 谢谢@Benmccormick。我已经这样做了,但是关于这个问题的有效性的讨论似乎是在这个问题下面的评论中进行的。我认为提出一个问题来创建一个平台来表达你的个人观点,因为事实并不是问题的真正目的。
- @理查德汉森,这似乎是一种欺骗积分系统的方法,尤其是当你的答案在音调上有如此大的差别和如此短的时间间隔时。使用您的问答模式,我们都可以使用以前的知识来提问和回答问题。在这一点上,你应该考虑写一篇博文,因为这是很多次更合适。问答专门寻求他人的知识。一篇博文展示了你自己。
- @乔希·布朗当然,去吧。如果它们是好问题(包括有用的、实际的问题、主题上的问题而不是重复的问题)和答案,那么您所做的就是好的。然而,提出好的问题是困难的。所以收集知识,而不是人们回答别人的问题。
- 正如其他人所指出的,提出问题并立即回答没有什么反对的,它是明确鼓励的。不过,我确实编辑了这个问题的基调,因为它有点好辩和固执己见。记住这个问题已经存在一年了。有点晚了,开始抱怨OP创建了问题,然后回答了他们自己的问题。
- @乔治斯托克:除了这个问题,更重要的是,下面的答案(同时发布)看起来很像是"火焰诱饵"的观点,尽管解释得很好。我认为这个问题可能是我所见过的"提出问题"的最好例子。
- @乔治斯托克:谢谢你的编辑,你改进了很多。
- 我同意。这样看起来好多了。
总结
默认情况下,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之间时,您必须养成输入的习惯:好的。
号
但是,对于所有版本的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 -p或git fetch --all -p不修改工作目录或索引,因此可以随时安全运行,即使您已经进行了阶段性和/或未进行阶段性更改。你可以暂停你正在做的事情,回顾别人的承诺,而不用担心你正在做的事情被藏起来或完成。git pull并没有给你这种灵活性。好的。重新定位到远程分支
一种常见的Git使用模式是执行git pull以引入最新的更改,然后引入git rebase @{u},以消除git pull引入的合并承诺。Git有一些配置选项,通过告诉git pull执行一个rebase而不是合并(见branch..rebase、branch.autosetuprebase和pull.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=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:好的。
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。好的。好啊。
- 对于一个经常被忽视的话题来说,这是一个很好的答案。我见过vanialla git pull在实践中给Git新手带来许多问题。问题:为什么remote update比fetch?领导是什么?在alias命令中执行吗?你也可以化名:git fetch --all -p; git merge --ff-only @{u}。
- @Brianz:git remote update -p相当于git fetch --all -p。我习惯输入git remote update -p,因为从前fetch没有-p选项。关于主导!,见git help config中alias.*的描述。它说,"如果别名扩展前面有一个感叹号,它将被视为shell命令。"
- 那么,为什么这种默认的有害行为是可以容忍的,并且是否有一个关于修复它的出色的bug报告呢?
- @PJC50:这是可以容忍的,因为Git开发人员不想破坏与现有脚本的向后兼容性。而且,还没有一个成熟的替代品,所以他们还不能贬低当前的行为。Git开发者不使用bug追踪器,而是使用邮件列表。见这里和这里的讨论。还要注意,这些更改将出现在下一个版本的Git中。
- 我写了一个比git up更详细的命令来做这个,git get。也许它是有用的。
- Git2.0增加了一个pull.ff配置,看起来实现了相同的功能,没有别名。
- 有些原因听起来像"当别人做疯狂的事情时,拉会导致问题"。不,这是疯狂的事情,比如重新平衡上游回购的承诺,这会导致问题。只有当您在本地提交尚未推送的提交时,IMO REBASE才是安全的。例如,当你在推之前拉的时候,重新平衡本地提交有助于保持你的历史线性(尽管线性历史不是那么重要)。不过,git up听起来是一个有趣的选择。
- 如果您只想更新您所在的分支,那么git up别名是否与刚才执行git pull --ff-only的别名类似?
- 一般来说,我发现"git fetch"后面跟"git rebase"是最干净、最安全的方法。FETCH显示了所有变更的分支,REBASE保留了提交历史记录。
- 另请参见stackoverflow.com/questions/5519007/…-您只能让pull执行fastforward/uncommit。
- 另一个选择是git pull --rebase。
- 您的大多数观点是因为您做了一些错误的事情:您试图在自己的工作分支中检查代码。这不是一个好主意,只需创建一个新的分支,pull--rebase=preserve,然后丢弃该分支(或者根据需要合并它)。
- @Funkaster在这里的观点很有意义,特别是关于:"难以回顾他人的承诺"。这不是大多数Git用户使用的审查流程,这是我从未在任何地方看到过的建议,这是标题下面描述的所有不必要的额外工作的原因,而不是git pull。
- 有人能解释一下@u的意思吗?
- 在与其他开发人员共享存储库的历史记录之后,重新编写存储库的历史记录听起来是个坏主意。如果遥控器的提交历史处于git pull不起作用的状态,那么您的操作是错误的。
- 很好的总结,但是它听起来像是从某个人的角度写的,他有一个非常特别(和奇怪)的Git工作流程。我的大部分Git经验都是在历史线性不相关的情况下进行的,并且重新平衡很少(如果有)完成。在这种情况下,这个答案基本上是无关紧要的。
- 确保你不只是盲目地遵循这个建议,它会导致尽可能多的问题,因为它试图解决。问题的原始海报也同时发布了这个答案,所以我不得不质疑他们所说的或他们试图实现的目标的有效性。
- 就我个人而言,我在git fetch之后做git rebase origin/master,如果它能快进,它就会快进,如果不能快进,它也会重新平衡。你只需要知道何时使用git pull/git merge/git rebase。
- @countfloortiles这个答案没有告诉你使用git push --force。Rebase并不总是重写公共历史。
- 仅供参考,@vladislavsburakovs,我发现,@u是@upstream的缩写,它指的是您的分支当前的上游远程。
- 自动修剪上游不再存在的树枝可能很危险。如果有人意外地删除了一个有价值的分支,那么您的origin/feature是一个备份(可能是唯一的备份),您不想丢失它。
从黑客新闻上的讨论中得出我的答案:
我很想用标题的更好法则来回答这个问题:为什么git pull被认为是有害的?不是的。
- 非线性本身并不坏。如果它们代表了实际的历史,那么它们是可以的。
- 意外地重新引入上游重新平衡的提交是错误地重写上游历史的结果。当沿着多个repo复制历史记录时,不能重写历史记录。
- 修改工作目录是一个有争议的有用性的预期结果,即面对hg/donole/darcs/other_-dvcs_-predating_-git的行为,但又不是本质上的坏。
- 合并需要暂停以检查其他人的工作,这也是git pull的预期行为。如果不想合并,应该使用git fetch。同样,与以前流行的DVC相比,这是Git的一个特性,但它是预期行为,而不是本质上的不良行为。
- 使远程分支的重新平衡变得困难是很好的。除非你绝对需要,否则不要重写历史。我一辈子都无法理解这种对(假的)线性历史的追求
- 不清理树枝是好事。每个回购都知道它想要持有什么。Git没有主从关系的概念。
- 我同意。git pull本身没有什么有害的。然而,它可能与一些有害的做法相冲突,比如想要重写历史,而不是严格必要的。但是Git是灵活的,所以如果你想以不同的方式使用它,无论如何都要这样做。但那是因为你(嗯,@Richard Hansen)想在Git中做一些不寻常的事情,而不是因为git pull是有害的。
- 不能再同意了。人们提倡使用git rebase,认为git pull有害?真的?
- 很高兴看到有人创建了一个以道德为轴心的图形,并将git命令分类为好、坏或介于两者之间的某个位置。这个图表在开发人员之间会有所不同,尽管它会说很多关于一个使用Git。
- 我对没有--rebase选项的git pull的问题是它创建的合并方向。当您查看diff时,合并中的所有更改现在都属于拉动者,而不是做出更改的人。我喜欢一个工作流,其中合并是为两个独立的分支保留的(A->B),所以合并提交很清楚地介绍了什么,并且重新分配是为同一分支上的最新状态保留的(远程A->本地A)
- 所以,也许真正的问题是——一个人如何保护自己不受坏习惯的影响?如果使用默认的"让我的回购与另一个同步"来列出冲突(历史和来源),并提供直接解决冲突的方法,那就太棒了。
- 那么,如果有人在别人或其他人之前几秒钟就拉了一把,你会得到什么呢?我认为这只是噪音,只是混淆了真正相关的历史。这甚至降低了历史的价值。好的历史应该是a)干净的,b)有重要的历史。
如果您正确使用git,则不会被认为是有害的。考虑到您的用例,我看到了它是如何对您产生负面影响的,但是您可以通过不修改共享历史来避免问题。
- 详细说明:如果每个人都在自己的分支机构工作(我认为这是使用git的正确方式),那么git pull就没有任何问题。在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=preserve和git pull --rebase --preserve-merges不一样,因为后者做得不对。
前面的另一个讨论解释了rebase的preserve merges变体的实际功能,以及它如何比常规的rebase复杂得多:git的"rebase--preserve merges"到底是做什么的(以及为什么?)