gpt4 book ai didi

git - 为什么 git cherry-pick 会比 git rebase 产生更少的冲突?

转载 作者:行者123 更新时间:2023-12-02 08:51:38 27 4
gpt4 key购买 nike

我经常 rebase 。偶尔的rebase尤其成问题(很多 merge 冲突),我在这种情况下的解决方案是 cherry-pick个人提交到主分支。我这样做是因为几乎每次我这样做时,冲突的数量都要少得多。

我的问题是为什么会这样。

为什么我 cherry-pick 时 merge 冲突较少比当我rebase ?

在我的心智模型中 rebase和一个 cherry-pick正在做同样的事情。

rebase 示例

A-B-C (master)
\
D-E (next)

git checkout next
git rebase master

产生
A-B-C (master)
\
D`-E` (next)

进而
git checkout master
git merge next

产生
A-B-C-D`-E` (master)

cherry-pick 示例
A-B-C (master)
\
D-E (next)

git checkout master
git cherry-pick D E

产生
A-B-C-D`-E` (master)

根据我的理解,最终结果是一样的。 (D 和 E 现在在 master 上,具有干净的(直线)提交历史记录。)

为什么后者( cherry-pick )会比前者( rebase )产生更少的 merge 冲突?

更新更新更新

我终于能够重现这个问题,现在我意识到我可能过于简化了上面的例子。这是我如何能够重现...

假设我有以下内容(注意额外的分支)
A-B-C (master)
\
D-E (next)
\
F-G (other-next)

然后我执行以下操作
git checkout next
git rebase master
git checkout master
git merge next

我最终得到以下结果
A-B-C-D`-E` (master)
\ \
\ D`-E` (next)
\
D-E
\
F-G (other-next)

从这里开始,我要么 rebase 要么 cherry-pick

rebase 示例
git checkout other-next
git rebase master

产生
A-B-C-D`-E`-F`-G` (master)

cherry-pick 实例
git checkout master
git cherry-pick F G

产生相同的结果
A-B-C-D`-E`-F`-G` (master)

但与 rebase 策略相比, merge 冲突要少得多。

终于复制了一个类似的例子,我想我明白为什么与 cherry-pick 相比, rebase 有更多的 merge 冲突,但我会把它留给其他人(他们可能会比我做得更好(更准确) ) 回答。

最佳答案

更新的答案(请参阅相关更新)

我认为这里发生的事情与选择要复制的提交有关。

让我们注意并搁置,git rebase 可以使用 git cherry-pickgit format-patchgit am 来复制一些提交。在大多数情况下,git cherry-pickgit am 应该达到相同的结果。 ( git rebase documentation 特别指出上游文件重命名是cherry-pick 方法的一个问题,而不是默认的基于 git am 的非交互式 rebase 方法。另请参阅下面原始答案中的各种括号注释和评论。)

这里要考虑的主要事情是要复制哪些提交。在手动方法中,您首先手动将提交 DE 复制到 D'E' ,然后手动将 FG 复制到 F'G' 。这是最少的工作量,正是我们想要的;这里唯一的缺点是我们必须做的所有手动提交识别。

使用命令时:

git checkout <branch> && git rebase <upstream>

您可以让 Git 自动执行查找要复制的提交的过程。这在 Git 做对时很好,但如果 Git 做错了就不行了。

那么 Git 是如何选择这些提交的呢?简单但有点错误的答案在这句话中(来自同一个文档):

All changes made by commits in the current branch but that are not in <upstream> are saved to a temporary area. This is the same set of commits that would be shown by git log <upstream>..HEAD; or by git log 'fork_point'..HEAD, if --fork-point is active (see the description on --fork-point below); or by git log HEAD, if the --root option is specified.


--fork-point 复杂性有点新,因为 git 2.something,但在这种情况下它不是“事件的”,因为您指定了 <upstream> 参数并且没有指定 --fork-point 。实际的 <upstream> 两次都是 master

现在,如果您实际运行每个 git log(使用 --oneline 使其更好):
git checkout next && git log --oneline master..HEAD

和:
git checkout other-next && git log --oneline master..HEAD

你会看到第一个列出了提交 DE — 太好了! - 但第二个列出了 DEFG 。哦哦, DE 出现了两次!

问题是,这有时有效。好吧,我在上面说“有点错误”。这是错误的原因,仅比之前的引用少两段:

Note that any commits in HEAD which introduce the same textual changes as a commit in HEAD..<upstream> are omitted (i.e., a patch already accepted upstream with a different commit message or timestamp will be skipped).



请注意,这里的 HEAD..<upstream> 与我们刚刚运行的 <upstream>..HEAD 命令中的 git log 相反,在那里我们看到 D -through- G

对于第一次 rebase , git log HEAD..master 中没有提交,因此没有可能被跳过的提交。这很好,因为没有要跳过的提交:我们将 EF 复制到 E'F' ,这正是我们想要的。

但是,对于第二次 rebase ,发生在第一次 rebase 完成之后, git log HEAD..master 将向您显示提交 E'F' :我们刚刚制作的两个副本。这些可能被跳过:他们是考虑跳过的候选者。

“可能跳过”不是“真正跳过”

那么 Git 如何决定它真正应该跳过哪些提交呢?答案是在 git patch-id 中,虽然它实际上是直接在 git rev-list 中实现的,这是一个非常花哨和复杂的命令。然而,这些都没有真正很好地描述它,部分原因是它很难描述。无论如何,这是我的尝试。 :-)

Git 在这里所做的是在去除识别行号后查看差异,以防补丁进入稍微不同的位置(由于早期补丁在文件中上下移动行)。它使用与文件相同的技巧——将文件内容转换为唯一的哈希值——将每个提交转换为“补丁 ID”。提交 ID 是一个唯一的哈希,用于标识一个特定的提交,并且始终是同一个特定的提交。补丁 ID 是一个不同的(但对于某些内容来说仍然是唯一的)哈希 ID,它总是标识“相同”的补丁,即删除和添加相同差异块的东西,即使它从不同的地方删除和添加它们地点。

为每次提交计算补丁 ID 后,Git 可以说:“啊哈,提交 D 和提交 D' 具有相同的补丁 ID!我应该跳过复制 D 因为 D' 可能是复制 D 的结果。”它可以对 EE' 做同样的事情。这通常有效——但是每当从 DD 的复制需要手动干预(修复 merge 冲突)时, D' 就会失败,并且每当从 EE 的复制需要手动干预时, E' 也会失败。

更智能的 rebase

这里需要的是一种“智能 rebase ”,它可以查看一系列分支并提前计算,它 promise 为所有要 rebase 的分支复制一次。然后,在完成所有副本后,此“智能 rebase ”将调整所有分支名称。

在这种特殊情况下 - 通过 D 复制 G - 实际上非常简单,您可以使用以下命令手动执行此操作:
$ git checkout -q other-next && git rebase master
[here rebase copies D, E, F, and G, perhaps with your assistance]

其次是:
$ git checkout next
[here git checks out "next", so that HEAD is ref: refs/heads/next
and refs/heads/next points to original commit E]
$ git reset --hard other-next~2

这是有效的,因为 other-next 命名提交 G' ,其父级是 F' ,其父级又是 E' ,这就是我们希望 next 指向的地方。由于 HEAD 指的是分支 nextgit reset 调整 refs/heads/next 指向提交 E' ,我们就完成了。

在更复杂的情况下,需要复制一次的提交并不都是整齐的线性:
                A1-A2-A3  <-- featureA
/
...--o--o--o--o--o--o--o <-- master
\
*--*--B3-B4-B5 <-- featureB
\
C3-C4 <-- featureC

如果我们想对所有三个特性进行“多重 rebase ”,我们可以独立于其他两个特性对 featureA 进行 rebase ——三个 A 提交都不依赖于除了早期的 A 提交之外的任何“非主”——而是复制五个 B 提交和四个 C 提交,我们必须复制 *B 的两个 C 提交,但只复制一次,然后将剩余的三个和两个提交(分别)复制到复制提交的尖端。

(编写这样的“智能 rebase ”是可能的,但将其正确集成到 Git 中,以便 git status 真正理解它,要困难得多。)

原答案

我很想看到一个可重复的例子。在大多数情况下,您的“头脑中”模型应该可以工作。不过,有一种已知的特殊情况。

交互式 rebase ,或将 -m--merge 添加到普通 git rebase ,实际上确实使用 git cherry-pick ,而默认的非交互式 rebase 使用 git format-patchgit am 代替。后者不太适合重命名检测。特别是,如果上游有文件重命名,1 交互式或 --merge rebase 的行为可能会有所不同(通常更好)。

(另外,请注意,两种 rebase ——面向补丁的版本和基于樱桃选择的版本——将跳过与 git patch-id 相同的提交,通过 git rev-list --left-only --cherry-pick HEAD...<upstream> 或等效的提交已经在上游提交。参见 the documentation for git rev-list ,特别是部分在 --cherry-mark--left-right 上,我认为这使这更易于理解。不过,这对于两种 rebase 应该是相同的;如果您手动挑选,则取决于您是否这样做。)

1更准确地说, git diff --find-renames 需要相信那里有重命名。通常,如果存在,它就会相信这一点,但由于它是通过比较树来检测它们的,因此这并不完美。

关于git - 为什么 git cherry-pick 会比 git rebase 产生更少的冲突?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38251859/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com