Git 将一个分支移动到另一个具有一个提交历史的分支

Git Moving a Branch to Another One with One Commit History

我正在我的功能分支上开发。开发完成后,我会将所有提交的代码移动到 master 分支。

但指定点是在这个操作之后主分支应该只有一个提交历史。

我会尽量用树来解释

--- BRANCH TREES WITHOUT MERGE OPERATION ---

A--B--C  (master-branch)
 \    ^--- After the merge operation commit will come after this point.
  \
   D--E--F--G  (feature-branch)


--- AFTER THE MERGE OPERATION EXPECTED BRANCH TREES ---

A--B--C--(H)  (master-branch)
 \        ^--- 'H' is the commit of all feature branch merge. 
  \             Include commits D, E, F, G.  
   \            This should have all source but, will shown one commit   message.
    \
     D--E--F--G  (feature-branch)

我怎样才能像这样合并这些分支?

我想你想做的是 'squash commits' 功能。

我只复制原始分支,然后以交互方式对其进行变基,这样我就可以压缩提交。

Git称之为"squash merge",你通过运行宁git merge --squash获得。与 Git 中的所有新提交一样,新提交将添加到当前分支,因此在这种情况下,您将首先进入 master-branch,然后是 运行 git merge --squash feature-branch

注意事项

有一些重要的事情需要了解。特别是,虽然与结果提交关联的 tree(源或工作树)是通过 merging(动词:动作,"to merge"),它不是合并提交(合并的形容词或名词形式)。

请记住,在 Git 中,提交有两个功能:

  • 他们历史。

  • 它们 包含 work-tree 快照(加上一些元数据:提交的人、时间和日志消息)。

一个正常的 git merge other 找到一个 合并基础 提交 运行ning git merge-base --all HEAD other,1 然后:

  • 组合两组差异:从基础到 HEAD 的差异总计 "what you changed",加上从基础到 other 的差异,总计到 "what they changed"。如果您更改了文件 one.txt 并且他们更改了文件 two.txt,则 Git 会进行两项更改,即您的 one.txt 和他们的 two.txt。如果您 都更改了文件 three.txt,Git 会尝试查看您的更改和他们的更改是否重叠,或者完全重复,或者完全独立。如果它们重叠,Git 会给你一个合并冲突。如果它们是独立的,则 Git 接受两个更改。如果它们是完全重复的,Git 将复制一份更改。结果 three.txtthree-way merge.

  • 的输出
  • 进行新提交,其 历史记录 说:"this commit is a merge commit, combining the work from the first parent HEAD and the second parent other".

A git merge --squash 其他人仍在执行合并操作,包括任何必需的 three-way 合并。然后它——没有明显的原因2——省略了 git commit 步骤,让你 运行 git commit,当你 运行 git commit,结果是 不是 合并提交:它的历史显示 "this commit is an ordinary commit, and its sole parent is HEAD"。换句话说,它的提交历史就好像您自己完成了 other 中的所有更改,除了任何重复的更改。

这就是你的要求,但这也是一个陷阱。如果您稍后决定需要在 feature-branch 上进行更多开发并再次合并,Git 将不会有显示现有合并的历史记录,并且不知道它可以使用先前合并的结果开始新的合并过程。

这最终意味着当您打算在 squash-merging 之后立即(或至少很快)删除 功能分支时,压缩合并最有效:

A---B----C      <-- main
 \
  D--E--F--G    <-- feature

(现在git checkout main && git merge --squash feature && git commit && git branch -D feature

A---B----C--H   <-- main
 \
  D--E--F--G    [abandoned]

这样一来,您就无法返回并在 G 之后添加更多使用 D-E-F-G 链的提交,这些提交稍后需要再次合并。如果你确实回去做了这样的提交,然后再次合并,合并基础提交将是 A,而不是 G,并且 Git 更有可能是 mis-merge,不小心复制了一些 D-E-F-G 更改。3


1如果git merge-base --all 找到多个合并基础,行为取决于合并策略。如果它发现 no 合并基础,旧版本 Git 和新版本 Git 的行为不同:较新的 Gits 停止并出现错误关于拒绝合并不相关的历史。

2常规合并将停止,但只有在 --no-commit 特别请求时才会停止。随后的 git commit 将进行合并提交,即 two-parent 提交。所以 git merge --squash 没有理由表现得好像你也指定了 --no-commit:如果你 真的想要 它停止,你可以 运行 git merge --squash --no-commit。 (这很可能是最早的 Git 脚本的遗留问题。)

3Git 的合并算法努力尝试只获取每个更改的一个副本,通常只会成功获取一个更改副本。也就是说,从 AG 之后的差异包括所有 D-E-F-G 的变化,以及从 AH 之后的差异包括所有D-E-F-G变化:但这就是"take one copy of the changes"意味着要处理的。

问题是当你对这个算法施加太大压力时,它就会失败。通常,结果是您必须手动解决的巨大合并冲突。

方法之一:

git checkout -b tmp feature-branch
git reset A --soft
git commit -m "feature"
#a new commit X is created. It's a squash of D, E, F, G
git checkout master-branch
git cherry-pick X
git branch -D tmp