我怎样才能做 `git reset --soft <some-commit>` 绕过合并提交?

How can I to do `git reset --soft <some-commit>` bypassing commits with merge?

我有两个分支 - mastersome:

  C2---M3---C4---C5 some
 /    /
C1---C3---C6---C7 master

其中 C1C2... - 它是提交并且 M3 - 合并提交 C3.

我想做:

git checkout some
git reset --soft C1

但我想绕过合并提交 M3

我该怎么做?

因此,您需要来自提交 C2C4C5 的更改,而不是来自合并 M3 的更改。我会用一个新的分支来做到这一点:

# Create a new branch started at C1
git checkout -b some-new C1

# Cherry-pick commits
git cherry-pick C2 C4 C5

# Delete the old branch and rename the new
git branch -D some
git branch -m some

首先,让我们注意提交是(完全)read-only 并且(大部分)是永久的。给定一个像这样的提交图片段:

  C2---M3---C4---C5  <-- some
 /    /
C1---C3---C6---C7  <-- master

无论 reset-ing 我们做什么,这些相同的提交将继续存在,至少会持续一段时间。如果我们添加一个也指向提交 C5 的保存名称(分支或标记名称),那么无论我们对名称 some 做什么,我们仍然可以命名提交 C5C4M3C2

其次,请记住每次提交都会存储一个完整的、独立的快照。这包括合并提交 M3。我们可以通过 运行ning git diff 将合并提交变成一组更改,以将快照的内容与提交的 parent 的内容进行比较——但是对于合并提交,它具有两个 parent,我们要从两个parent中选一个。

第三,既然你提到了git reset --soft,让我们注意,除了提交之外,Git给了我们一个work-tree我们实际工作(并可以查看提交),以及一个 index——也称为 staging areacache ——我们在提交之前用来构建每个提交。如果我们 运行:

git checkout some

Git 将从名称 some 当前指向的提交内容中填充索引和 work-tree,即 C5,并附上我们的 HEAD 到名字 some。让我们附加第二个名字,tmp,此时,使用 git branch tmp:

  C2---M3---C4---C5  <-- some (HEAD), tmp
 /    /
C1---C3---C6---C7  <-- master

运行 git reset --soft <hash-of-C1> 此时将使名称 some 指向提交 C1,同时保持我们的索引和 work-tree 不变。也就是说,索引和 work-tree 内容将继续匹配 C5:

  C2---M3---C4---C5  <-- tmp
 /    /
C1---C3---C6---C7  <-- master
 .
  ... <-------------- some (HEAD)

如果我们现在进行新的提交,通过 运行ning git commit,我们将得到一个新的提交 C8,其内容与 C5 的内容匹配。这是因为新的提交是根据索引的内容进行的,它与 C5 的内容相匹配。 C8 的 parent 将是 C1,然而,给我们:

  C2---M3---C4---C5  <-- tmp
 /    /
C1---C3---C6---C7  <-- master
 \
  C8  <-------------- some (HEAD)

之后 git diff some tmp 将完全没有区别。

你在评论中提到你想要的,在索引中或者可能作为新提交 C8,是 content 可以通过 cherry-picking 实现C2C4C5 在提交 C1 之上。你不能单独使用 git reset 来获得它。

由 cherry-picking

构建

最直接的方法是设置一个分支名称指向提交C1,同时设置索引和work-tree匹配C1:

git checkout -b new <hash-of-C1>

然后使用 git cherry-pick,可选地使用 -n,如 ,我在输入时输入。您也可以使用 git reset --hard,而不是 git reset --soft,移动 some 指向 C1,同时保留提交名称 C5(如上图中使用 tmp)。然后 name 您构建的新分支,通过 cherry-picking 三个所需的提交,将是 some.

通过还原构建

最后,如果您愿意,您可以尝试通过 subtractive 过程构建您的新提交。这可能有点容易出错,因为它取决于合并的内容 M3,1 但它的工作原理如下:

  • 我们知道C5实际上就是C1加上C2-as-changeset加上C1-vs -C3为changeset-via-merge,加上C4-as-changeset,加上C5-as-changeset.

  • 我们可以通过 git diff-ing 直接计算 C1-vs-C3

  • 更好:我们可以计算 C1-vs-C3 间接 ,通过 git diffing C2M3 .这将处理某些重复情况,其中 C1-vs-C3C1-vs-C2 具有 相同的 更改, 因此它们在 C2-vs-M3.

  • 中不是 doubly-included
  • 我们可以(至少尝试)随时reverse-apply任何我们喜欢的补丁。也就是说,将提交变成变更集(通过与 parent 进行比较),而不是将这些更改复制到 没有 的某个提交中,我们可以 撤消 某些提交中 确实 的更改。执行此操作的命令是 git revert.

然后,假设我们像以前一样检查提交 some,以便我们只有初始设置:

  C2---M3---C4---C5  <-- some (HEAD)
 /    /
C1---C3---C6---C7  <-- master

现在我们运行git revert -m 1 <hash-of-M3>。这告诉 Git 比较 C2M3 以查看发生了什么变化。2 结果是一个新的提交 C8:

  C2---M3---C4---C5---C8  <-- some (HEAD)
 /    /
C1---C3---C6---C7  <-- master

其中很可能包含您想要的 内容C1 作为快照加上 C2 作为变更集以获取 C2 作为内容,加上 M3 vs C2 作为变更集以获取 M3 作为内容(但最终 minus M3 vs C2 作为最后的变更集),等等。由于 C8 un-does M3 做了什么,C8 应该有想要的内容。

此时你可以,如果你愿意,git reset --soft <hash-of-C1>,留下索引,work-tree设置你想要的方式,然后运行 git commit创建提交C9:

  C2---M3---C4---C5---C8
 /    /
C1---C3---C6---C7  <-- master
 \
  C9  <-------------- some (HEAD)

由于没有 name 可以找到它们,这里图表顶行的所有提交都变得不可见,大约 30 天后,Git当它 运行 的垃圾收集器真正删除它们时。


1特别是,如果我们使用 git revert <hash-of-C3>,我们可能会还原太多。这就是我们 git revert -m 1 <hash-of-M3> 在这里的原因。

2这假设 M3 的第一个 parent 实际上是 C2。在任何正常的提交增长过程中,它都会,但它值得 double-checking.

我喜欢 cherry-picking 方法,但另一种方法是:

git checkout some
git rebase --interactive --preserve-merges C2

...然后编辑呈现的选择列表,将合并提交 M3 行前面的 pick 更改为 drop。您最终会得到 C5->C4->C2 作为 some 上的一组提交,这些提交也不在 master.