ChatGPT解决这个技术问题 Extra ChatGPT

Git 工作流程和变基与合并问题

几个月来,我和其他开发人员一起在一个项目中使用了 Git。我在 SVN 方面有几年的经验,所以我想我给这段关系带来了很多包袱。

我听说 Git 非常适合分支和合并,但到目前为止,我还没有看到它。当然,分支非常简单,但是当我尝试合并时,一切都变得糟糕透顶。现在,我已经习惯了 SVN,但在我看来,我只是将一个低于标准的版本控制系统换成了另一个。

我的搭档告诉我,我的问题源于我想随意合并的愿望,并且在许多情况下我应该使用 rebase 而不是合并。例如,这是他制定的工作流程:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

本质上,创建一个特性分支,总是从主分支变基到分支,然后从分支合并回主分支。需要注意的重要一点是,分支始终保持在本地。

这是我开始的工作流程

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

有两个本质区别(我认为):我总是使用合并而不是变基,并且我将我的功能分支(和我的功能分支提交)推送到远程存储库。

我对远程分支的推理是我希望在工作时备份我的工作。我们的存储库会自动备份,如果出现问题可以恢复。我的笔记本电脑没有,或者没有那么彻底。因此,我讨厌在我的笔记本电脑上有没有在其他地方镜像的代码。

我对合并而不是 rebase 的理由是,merge 似乎是标准的,而 rebase 似乎是一个高级功能。我的直觉是我想做的不是高级设置,所以 rebase 应该是不必要的。我什至仔细阅读了关于 Git 的新实用程序设计书,它们广泛地涵盖了合并,几乎没有提到变基。

无论如何,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回 master 时,一切都变得糟糕透顶。与本应无关紧要的事情发生了很多冲突。这些冲突对我来说毫无意义。我花了一天的时间整理一切,最终以强制推送到远程 master 达到高潮,因为我的本地 master 已经解决了所有冲突,但远程 master 仍然不高兴。

像这样的“正确”工作流程是什么? Git 应该让分支和合并变得超级容易,而我只是没有看到它。

2011-04-15 更新

这似乎是一个非常受欢迎的问题,所以我想我会用我第一次问起的两年经验来更新。

事实证明,最初的工作流程是正确的,至少在我们的例子中是这样。换句话说,这就是我们所做的并且它有效:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

事实上,我们的工作流程有点不同,因为我们倾向于进行 squash 合并而不是原始合并。 (注意:这是有争议的,见下文。)这允许我们将整个功能分支变成 master 上的单个提交。然后我们删除我们的功能分支。这允许我们在 master 上逻辑地构建我们的提交,即使它们在我们的分支上有点混乱。所以,这就是我们要做的:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

Squash Merge Controversy - 正如一些评论者所指出的,squash 合并将丢弃您功能分支上的所有历史记录。顾名思义,它将所有提交压缩为一个。对于小功能,这是有道理的,因为它将它压缩成一个包。对于更大的功能,这可能不是一个好主意,特别是如果您的个人提交已经是原子的。这真的取决于个人喜好。

Github 和 Bitbucket(其他?)Pull Requests - 如果您想知道合并/rebase 与 Pull Requests 的关系,我建议您按照上述所有步骤操作,直到您准备好合并回 master。无需手动与 git 合并,您只需接受 PR。请注意,这不会进行 squash 合并(至少默认情况下不会),但非 squash、非快进是 Pull Request 社区中公认的合并约定(据我所知)。具体来说,它是这样工作的:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

我开始爱上 Git,再也不想回到 SVN。如果你在挣扎,那就坚持下去,最终你会看到隧道尽头的曙光。

不幸的是,新的 Pragmstic Programming 书主要是使用 Git 编写的,同时仍在 SVN 中思考,在这种情况下它误导了你。在 Git 中,rebase 尽可能让事情变得简单。您的经验可以告诉您,您的工作流程在 Git 中不起作用,而不是 Git 不起作用。
在这种情况下,我不建议进行 squash 合并,因为它不会保存有关合并内容的信息(就像 svn,但这里没有合并信息)。
喜欢底部的注释,我有类似的与 Git 斗争的经历,但现在很难想象不使用它。也感谢您的最终解释,对rebase理解有很大帮助
完成该功能后,在将 new_feature 合并到 master 之前,您不应该最后一次 rebase 吗?
您的工作流程会丢失已删除分支的所有提交历史记录:(

E
Edward Anderson

TL;博士

git rebase 工作流不会保护您免受不擅长解决冲突的人或习惯于 SVN 工作流的人的伤害,如 Avoiding Git Disasters: A Gory Story 中所建议的那样。它只会让冲突解决对他们来说更加乏味,并且更难从糟糕的冲突解决中恢复过来。相反,请使用 diff3,这样一开始就不会那么困难。

Rebase 工作流程并不适合解决冲突!

我非常支持清理历史记录。但是,如果我遇到冲突,我会立即中止变基并进行合并!人们推荐使用 rebase 工作流程作为解决冲突的合并工作流程的更好替代方案,这真的让我很生气(这正是这个问题的意义所在)。

如果它在合并期间“全部下地狱”,它会在 rebase 期间“下地狱”,并且可能还有更多的地狱!原因如下:

原因 #1:解决一次冲突,而不是每次提交一次

当您变基而不是合并时,您将不得不执行冲突解决,次数与您提交变基的次数一样多,对于相同的冲突!

真实场景

我从 master 分支出来重构一个分支中的复杂方法。我的重构工作总共包含 15 次提交,因为我正在努力重构它并获得代码审查。我重构的一部分涉及修复以前存在于 master 中的混合制表符和空格。这是必要的,但不幸的是它会与之后在 master 中对此方法所做的任何更改相冲突。果然,当我正在研究这个方法时,有人对 master 分支中的相同方法进行了简单、合法的更改,该更改应该与我的更改合并。

当需要将我的分支与 master 合并时,我有两个选择:

git merge:我遇到了冲突。我看到他们对 master 所做的更改并将其与我的分支(的最终产品)合并。完毕。

git rebase:我的第一次提交发生了冲突。我解决了冲突并继续变基。我与第二次提交发生冲突。我解决了冲突并继续变基。我与第三次提交发生冲突。我解决了冲突并继续变基。我与第四次提交发生冲突。我解决了冲突并继续变基。我与第五次提交发生冲突。我解决了冲突并继续变基。我与第六次提交发生冲突。我解决了冲突并继续变基。我与我的第七次提交发生冲突。我解决了冲突并继续变基。我与我的第八次提交发生冲突。我解决了冲突并继续变基。我与第九次提交发生冲突。我解决了冲突并继续变基。我与第十次提交发生冲突。我解决了冲突并继续变基。我与第十一次提交发生冲突。我解决了冲突并继续变基。我与第十二次提交发生冲突。我解决了冲突并继续变基。我与我的第十三次提交发生冲突。我解决了冲突并继续变基。我与我的第十四次提交发生冲突。我解决了冲突并继续变基。我与我的第十五次提交发生冲突。我解决了冲突并继续变基。

如果这是您首选的工作流程,那您一定是在开玩笑。所需要的只是一个空白修复,它与对 master 所做的更改发生冲突,并且每次提交都会发生冲突并且必须解决。这是一个只有空格冲突的简单场景。天堂禁止您发生真正的冲突,涉及跨文件的主要代码更改,并且必须多次解决。

有了你需要做的所有额外的冲突解决,它只会增加你犯错的可能性。但是 git 中的错误很好,因为您可以撤消,对吗?当然除了...

原因 #2:使用 rebase,没有撤消!

我想我们都同意解决冲突可能很困难,而且有些人很不擅长。它很容易出错,这就是为什么 git 可以很容易地撤消它的原因!

当你合并一个分支时,git 会创建一个合并提交,如果冲突解决不顺利,它可以被丢弃或修改。即使您已经将错误的合并提交推送到公共/权威存储库,您也可以使用 git revert 撤消合并引入的更改,并在新的合并提交中正确地重做合并。

当您重新设置分支时,如果冲突解决可能出错,您就完蛋了。现在每个提交都包含错误的合并,您不能只重做 rebase*。充其量,您必须返回并修改每个受影响的提交。不好玩。

在 rebase 之后,无法确定提交的最初部分是什么,以及由于冲突解决不当而引入的内容。

*如果您可以从 git 的内部日志中挖掘旧的 refs,或者如果您创建第三个分支指向变基之前的最后一次提交,则可以撤消变基。

摆脱冲突解决:使用 diff3

以这个冲突为例:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

从冲突来看,不可能说出每个分支发生了什么变化或它的意图是什么。在我看来,这是解决冲突令人困惑和困难的最大原因。

diff3来救援!

git config --global merge.conflictstyle diff3

当您使用 diff3 时,每个新的冲突都会有一个第 3 部分,即合并的共同祖先。

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

首先检查合并的共同祖先。然后比较每一方以确定每个分支的意图。您可以看到 HEAD 将 EmailMessage 更改为 TextMessage。它的意图是改变用于TextMessage的类,传递相同的参数。您还可以看到 feature-branch 的意图是为 :include_timestamp 选项传递 false 而不是 true。要合并这些更改,请结合两者的意图:

TextMessage.send(:include_timestamp => false)

一般来说:

比较每个分支的共同祖先,并确定哪个分支具有最简单的更改 将该简单更改应用到另一个分支的代码版本,使其包含更简单和更复杂的更改您刚刚将更改合并到的那个

替代:通过手动应用分支的更改来解决

最后,即使使用 diff3,有些冲突也很难理解。尤其是当 diff 发现在语义上不常见的共同行时(例如,两个分支碰巧在同一位置有一个空行!),就会发生这种情况。例如,一个分支更改类主体的缩进或重新排序类似的方法。在这些情况下,更好的解决策略可以是检查合并任一侧的更改并手动将差异应用到另一个文件。

让我们看看在合并 origin/feature1 where lib/message.rb 冲突的场景中如何解决冲突。

确定我们当前签出的分支(HEAD 或 --ours)或我们正在合并的分支(origin/feature1 或 --theirs)是否更易于应用。将 diff 与三点 (git diff a...b) 一起使用可以显示自上次与 a 分歧以来 b 发生的变化,或者换句话说,将 a 和 b 的共同祖先与 b 进行比较。 git diff HEAD...origin/feature1 -- lib/message.rb # 显示 feature1 的变化 git diff origin/feature1...HEAD -- lib/message.rb # 显示我们分支的变化 查看更复杂的文件的版本。这将删除所有冲突标记并使用您选择的一侧。 git checkout --ours -- lib/message.rb # 如果我们分支的更改更复杂 git checkout --theirs -- lib/message.rb # 如果 origin/feature1 的更改更复杂 复杂的更改签出后,向上拉较简单更改的差异(参见步骤 1)。将此差异中的每个更改应用于冲突文件。


一次合并所有冲突如何比单个提交更好?我已经从合并单个提交中遇到了问题(尤其是那些不将提交分解为逻辑部分并提供足够的验证测试的人)。此外,在备份选项方面,rebase 并不比合并差,智能使用交互式 rebase 和像 tortoisegit 之类的工具(它允许选择要包含的提交)将有很大帮助。
我觉得我在#1中解决了原因。如果单个提交在逻辑上不一致,则更有理由合并逻辑一致的分支,这样您就可以真正理解冲突。如果提交 1 有错误并且提交 2 修复了它,那么合并提交 1 将会令人困惑。有正当的理由你可能会连续遇到 15 次冲突,就像我上面概述的那样。此外,您关于变基不会变得更糟的论点有些毫无根据。 Rebase 将错误的合并混合到原始的良好提交中,并且不会留下好的提交让您再次尝试。合并可以。
我完全同意你的观点。伟大的职位;这清除了一些事情。我想知道 rerere 在这里是否有任何帮助。另外,感谢关于使用 diff3 的建议,我现在肯定会打开那个。
+1 仅告诉我有关 diff3 的信息 - 多久看到一次难以理解的冲突,诅咒负责不告诉我共同祖先必须说什么的人。非常感谢。
这应该是公认的答案。 rebase 工作流程也很糟糕,因为它隐藏了代码库在某个时间点存在巨大差异的事实,这对于了解您是否想了解您正在查看的代码是如何编写的可能很有用。只有不冲突的小分支才应该重新建立在 master 上。
J
James Wright

“冲突”是指“同一内容的平行演化”。因此,如果它在合并期间“全部下地狱”,则意味着您在同一组文件上进行了大规模的演变。

变基比合并更好的原因是:

你用主人之一重写你的本地提交历史(然后重新应用你的工作,然后解决任何冲突)

最终的合并肯定是一个“快进”的合并,因为它将拥有 master 的所有提交历史,加上只有你的更改才能重新应用。

我确认在这种情况下正确的工作流程(公共文件集的演变)首先是变基,然后是合并。

但是,这意味着,如果您推送本地分支(出于备份原因),则不应被其他任何人拉取(或至少使用)该分支(因为提交历史记录将被连续的 rebase 重写)。

关于该主题(变基然后合并工作流程),barraponto 在评论中提到了两个有趣的帖子,均来自 randyfay.com

Git 的 Rebase 工作流:提醒我们先获取,rebase:

使用这种技术,您的工作总是在公共分支之上进行,就像一个与当前 HEAD 保持同步的补丁一样。

(类似的技术 exists for bazaar

避免 Git 灾难:一个血腥故事:关于 git push --force 的危险(例如,而不是 git pull --rebase)


有关允许变基 共享的技术,请参阅softwareswirl.blogspot.com/2009/04/…
randyfay.com/node/91randyfay.com/node/89 是精彩的读物。这些文章让我了解了我的工作流程的磨损,以及理想的工作流程是什么。
只是为了直截了当,从主分支重新定位到您的本地基本上是为了更新您的本地可能错过的任何历史,而主在任何类型的合并后都知道?
@dtan 我在这里描述的是在 master 之上重新定位本地。您并没有完全更新本地历史记录,而是在 master 之上重新应用本地历史记录,以解决本地分支内的任何冲突。
A
Alex Gontmakher

在我的工作流程中,我尽可能地变基(并且我尝试经常这样做。不让差异累积会大大减少分支之间冲突的数量和严重性)。

然而,即使在大部分基于 rebase 的工作流程中,也有合并的地方。

回想一下,合并实际上创建了一个具有两个父节点的节点。现在考虑以下情况:我有两个独立的功能分支 A 和 B,现在想在依赖 A 和 B 的功能分支 C 上开发东西,而 A 和 B 正在接受审查。

然后我要做的是:

在 A 之上创建(和结帐)分支 C。将其与 B 合并

现在分支 C 包含来自 A 和 B 的更改,我可以继续开发它。如果我对 A 进行任何更改,那么我会通过以下方式重建分支图:

在 A 的新顶部创建分支 T 与 B 合并 T 将 C 重新设置到 T 上删除分支 T

这样我实际上可以维护任意的分支图,但是做一些比上述情况更复杂的事情已经太复杂了,因为没有自动工具在父项更改时进行变基。


你可以通过变基来达到同样的效果。此处实际上不需要合并(除非您不想复制提交 - 但我几乎不认为这是一个论点)。
事实上,我不想重复提交。我想保持我工作的飞行结构尽可能干净。但这是个人品味的问题,不一定适合每个人。
我100%同意第一段。 (@Edward 的答案适用于并非如此的情况,但我宁愿让世界上的所有项目都像你建议的那样工作)。其余的答案似乎有点牵强,因为在 A 和 B 正在进行时在 C 上工作已经有点冒险了(至少在某种程度上它真的取决于 A 和 B),甚至最后你可能不会保留合并(C 将在最新和最伟大的基础上重新建立)。
o
ololuki

几乎在任何情况下都不要使用 git push origin --mirror。

它不会询问您是否确定要这样做,而且您最好确定,因为它会清除所有不在本地机器上的远程分支。

http://twitter.com/dysinger/status/1273652486


或者不要做你不确定结果会怎样的事情?我曾经管理的一台机器在 MOTD 中有 Instructions to this machine may lead to unintended consequences, loss of work/data, or even death (at the hands of the sysad). Remember that you are solely responsible for the consequences of your actions
如果您确实有镜像仓库,请使用它(尽管在我的情况下,它现在由源仓库的特殊用户在 post-receive 挂钩上执行)
P
Pat Notz

在你的情况下,我认为你的伴侣是正确的。变基的好处在于,对于局外人来说,您的更改看起来就像它们都是以干净的顺序发生的一样。这表示

您的更改很容易查看

您可以继续进行良好的小型提交,但您可以一次将这些提交的集合公开(通过合并到 master 中)

当您查看公共主分支时,您会看到不同开发人员针对不同功能的不同系列提交,但它们不会全部混合

您仍然可以继续将您的私有开发分支推送到远程存储库以进行备份,但其他人不应将其视为“公共”分支,因为您将进行变基。顺便说一句,执行此操作的简单命令是 git push --mirror origin

文章 Packaging software using Git 很好地解释了合并与变基之间的权衡。这是一个有点不同的上下文,但原则是相同的——它基本上归结为你的分支是公共的还是私有的,以及你计划如何将它们集成到主线中。


使用 git 打包软件的链接不再起作用。我找不到编辑原始答案的好链接。
您不应镜像到 origin,而应镜像到第三个专用备份存储库。
T
Taylan Aydinli

看了你的解释后,我有一个问题:难道你从来没有做过

git checkout master
git pull origin
git checkout my_new_feature

在您的功能分支中执行“git rebase/merge master”之前?

因为您的 master 分支不会自动从您朋友的存储库中更新。您必须使用 git pull origin 执行此操作。即,也许你总是从一个永不改变的本地主分支变基?然后到了推送时间,您正在推送一个存储库,其中包含您从未见过的(本地)提交,因此推送失败。


P
Peter Mortensen

无论如何,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回 master 时,一切都变得糟糕透顶。与本应无关紧要的事情发生了很多冲突。这些冲突对我来说毫无意义。我花了一天的时间整理一切,最终以强制推送到远程 master 达到高潮,因为我的本地 master 已经解决了所有冲突,但远程 master 仍然不高兴。

在您的合作伙伴和您建议的工作流程中,您都不应该遇到没有意义的冲突。即使您有,如果您遵循建议的工作流程,那么在解决后不应要求“强制”推送。这表明您实际上并没有合并您要推送的分支,而是必须推送一个不是远程提示的后代的分支。

我认为你需要仔细看看发生了什么。其他人是否可以(有意或无意地)在您创建本地分支和您尝试将其合并回本地分支之间重绕远程主分支?

与许多其他版本控制系统相比,我发现使用 Git 涉及的工具更少,并且允许您着手解决对源流至关重要的问题。 Git 不执行魔法,因此冲突的更改会导致冲突,但它应该通过跟踪提交父级来轻松完成写入操作。


您是在暗示 OP 在他的过程中有一些未发现的变基或错误,对吗?
E
Esteban Herrera

“即使你是一个只有几个分支的开发人员,养成使用 rebase 和正确合并的习惯也是值得的。基本的工作模式如下所示:

从现有分支 A 创建新分支 B

在分支 B 上添加/提交更改

来自分支 A 的变基更新

将分支 B 的更改合并到分支 A"

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/


P
Pepe

根据我的观察,即使在合并之后,git merge 也倾向于保持分支分离,而 rebase 然后合并将其合并为一个分支。后者更清晰,而在前者中,即使在合并之后也更容易找出哪些提交属于哪个分支。


B
Bombe

使用 Git 没有“正确”的工作流程。使用任何漂浮你的船。但是,如果您在合并分支时经常遇到冲突,也许您应该与其他开发人员更好地协调您的工作?听起来你们两个一直在编辑相同的文件。此外,请注意空格和颠覆性关键字(即“$Id$”等)。


W
WesternGun

我只使用 rebase 工作流,因为它在视觉上更清晰(不仅在 GitKraken 中,而且在 Intellij 和 gitk 中,但我最推荐第一个):你有一个分支,它起源于 master,它去回到主人。当图表干净漂亮时,您会知道没有什么会下地狱的,永远

https://i.stack.imgur.com/B85dm.png

我的工作流程与您的工作流程几乎相同,但只有一点不同:我在 rebase 我的分支到 master 上的最新更改之前 squash 提交到本地分支中的一个,因为:

rebase 根据每个提交工作

这意味着,如果您有 15 次提交更改了与 master 相同的行,则如果您不压缩,则必须检查 15 次,但重要的是最终结果,对吧?

所以,整个工作流程是:

签出到master并拉取以确保您拥有最新版本从那里,创建一个新分支在那里做您的工作,您可以自由提交多次,并推送到远程,不用担心,因为它是您的分支。如果有人告诉你,“嘿,我的 PR/MR 被批准了,现在它被合并到 master”,你可以获取它们/拉取它们。你可以在任何时候做,也可以在第 6 步做。在做完所有工作后,提交它们,如果你有几个提交,则压缩它们(它们都是你的工作,你改变多少次代码行无关紧要;唯一重要的是最终版本)。推不推,无所谓。结帐到 master,再次拉取以确保您在本地拥有最新的 master。您的图表应与此类似:

https://i.stack.imgur.com/GntcK.png

如您所见,您位于本地分支上,该分支源于 master 上的过时状态,而 master(本地和远程)随着您同事的变化而向前发展。

结帐回您的分支,然后变基为 master。您现在将只有一次提交,因此您只需解决一次冲突。(在 GitKraken 中,您只需将您的分支拖到 master 并选择“Rebase”;我喜欢它的另一个原因。)之后,您将像:

https://i.stack.imgur.com/tCSGE.png

所以现在,您拥有最新 master 上的所有更改,以及您的分支上的更改。您现在可以推送到您的遥控器,如果您之前推送过,您将不得不强制推送; Git 会告诉你不能简单地快进。这很正常,因为 rebase,你已经改变了你的分支的起点。但你不应该害怕:明智地使用武力,但不要害怕。最后,远程也是你的分支,所以即使你做错了也不会影响 master。创建 PR/MR 等到审核通过,master 会有你的贡献。恭喜!因此,您现在可以结帐以掌握、提取更改并删除本地分支以清理图表。如果在将远程分支合并到 master 时没有这样做,远程分支也应该被删除。

最终的图表再次清晰明了:

https://i.stack.imgur.com/CljEO.png