Git 从分支中减去推送的提交
Git subtract pushed commits from a branch
我在同一分支上为一个模块共同开发了一些代码,同时我也在另一个分支上与同事的模块进行了集成。基本上我最终同时修改了两个模块的代码,因为我让这两个模块相互通信。现在我想将所有代码一起提交,但不希望将分支中的内容混为一谈。
- 假设我的模块是 "Apple",在分支 A
上开发
- 我同事的代码是"Banana",开发于Branch B
- 我想在分支 C
上集成 "Cocktail"
所以基本上我在 A 上进行了开发,从 B 中挑选了我想要的更改,并且已经进行了测试集成。现在我想分离提交,让分支 A 只包含 "Apple",分支 B 只包含 "Banana",分支 C 包含集成代码。我不想重命名分支,所以我已经通过 cherry-pick 将所有更改从分支 A 复制到 C。另外,我已经挑选了从分支 A 到 B 的相关更改。
现在剩下要做的就是从分支 A 中减去提交,这样就只剩下 "Apple" 了。我正在寻找最好的方法来做到这一点。我不相信 git revert
是正确的选择,我想我可以做一个交互式 rebase 并删除我不想要的 A 提交,但我正在寻找验证这是正确的方向,或者如果需要其他东西。
分支名称并不重要,除了对人类而言。 (你是人类吗?如果是的话,我们可能会遇到一些障碍。)
对 Git 重要的是 提交 。提交 是 存储库中的历史记录。每个提交都有一个唯一的哈希 ID,这些哈希 ID 是 Git 查找提交的方式(我们稍后会看到一个重要的例外)。散列 ID 又大又丑,看起来 随机,人类基本上不可能使用,这就是我们使用分支名称的原因。
每个提交都包含所有文件的完整快照(好吧,所有出现在该提交中的文件)。而且,每个提交都包含一些元数据——关于提交的信息——例如提交者和时间,以及对 Git 本身非常重要的直接父提交的原始哈希 ID。所以这让 Git 从 last 提交开始并向后工作。
也就是说,假设哈希为 H
的提交是某个链中的 last 提交,标记为 branch-A
之类的分支名称:
... <-F <-G <-H <--branch-A
name branch-A
保存链中最后一次提交的哈希 ID,即提交 H
。提交 H
本身持有早期提交 G
的哈希 ID,后者持有早期提交 F
的哈希 ID,依此类推。
诀窍是 name 包含 last 提交的哈希 ID。没有其他简单的方法可以找到最后一个!最后一个找到倒数第二个,找到倒数第三个,依此类推,但不只是人类需要这个名字:Git 也需要它,找到 最后提交。
So basically I did my development on A, cherry-picked the changes I wanted from B, and already did the tested integration.
当您使用 git cherry-pick
时,您是在告诉 Git 复制 (效果和一些元数据)提交。所以你和任何在 B 上工作的人都是从一些共同的起点开始的,比如提交 H:
I--J <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
您提到您使用 git cherry-pick
从其他分支 复制 一些提交。将 K
复制到一个新的提交,我将其称为 K'
以表明它与 K
的相似程度,给出:
I--J--K' <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
请注意 name branch-A
现在如何指向新的 last 提交,现在是 K'
.提交 K'
具有所有内容的完整快照,但是将 K'
与 J
进行比较,您会看到与比较 更改 相同=33=] 到 H
。 K'
的作者和日志消息也将与 H
的作者和日志消息匹配,除非您告诉 Git 更改它们。当然,K'
的父哈希是J
,而K
的父哈希是H
。
您也可以添加更多提交,就像您可能所做的那样:
I--J--K'--L <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
Now all that's left to do is subtract commits from branch A so that only "Apple" is left. I'm looking for the best way to do this.
不一定有最好的方法。但是 Git 非常适合 add 提交到一个分支,更不用说 remove 从一个分支提交。如果你确实想删除一个提交,你可以告诉 Git 强制 name 向后移动。不再是 last 的提交现在无法找到。例如,如果我们强制名称 branch-A
向后移动以指向提交 I
,我们将得到:
J--K'--L [abandoned]
/
I <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
使用 Git 的常规提交查看工具,例如 git log
,我们不会 看到 J
和 K'
和 L
,所以看起来它们已经消失了。1 查看者从 last 提交开始,由分支名称找到,并向后工作。
无论如何,这里的大问题是 Git 是为 add 提交构建的。你可以让你自己的 Git 向后移动你自己的分支名称,例如使用 git reset
或 git branch -f
,但这不会使任何 other Git,您已向其发送提交,将 其 名称向后移动。
1如果我们让他们在这个 unreachable 状态停留足够长的时间,提交 J
和 K'
最终会被垃圾收集。 git gc
命令,它 Git 运行 时不时地自己执行,一般来说,一旦它们已经这样运行了至少 30 天,就会执行此操作——所以你至少得到 30天,再加上 Git 到 运行 git gc
需要多长时间才能改变你的想法并让他们回来。
一个简单的解决方案
处理此问题的一个简单方法是使用新名称。由于 Git 不关心名称,我们可以只调用它 neo-A
而不是 branch-A
。我们将 neo-A
设置为指向现有提交 H
,您最初从哪里开始:
I--J--K'--L <-- branch-A
/
...--G--H <-- common-starting-point, neo-A
\
K <-- branch-B
现在,对于 neo-A
,我们将 添加 提交,一次一个。我们查看现有的提交。 I
是我们喜欢的,所以我们可以复制它,或者直接使用它,因为它很好。让我们做后者——直接使用它——通过使 neo-a
朝 L
的方向前进一步,Git "likes" 使分支名称移动的方式。 (当然,向 L
的方向移动很重要,而不是 K
,但这很容易,因为我们处于控制之中。)
J--K'--L <-- branch-A
/
I <-- neo-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
我们 H
到 L
方向的下一个提交是提交 J
。那一个也很好:我们可以按原样使用名称 neo-A
将 J
前进一步,给出:
K'--L <-- branch-A
/
I--J <-- neo-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
K'
是个问题:我们不想要它。所以我们只是不在这里前进。不过我们确实想要 L
,现在我们 必须 复制它,因为现有的 L
指向 K'
,我们想要一个新的指向 K'
改为 J
。所以这次我们需要使用git cherry-pick
,产生:
K'--L <-- branch-A
/
I--J--L' <-- neo-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
我们会继续,但此时我们已经完成了所有提交,也就是说,我们完成了。
我们现在可以安全地 git push
neo-A
name。那么我们只需要说服大家停止使用旧名称即可。
让这件事更容易发生
上述方法的缺点是我们必须 "move the name forward" 一次一个步骤。如果我们可以创建 neo-A
并让我们的 Git 完成所有工作,那就太好了。事实证明,我们可以。 git rebase
命令拥有我们喜欢的所有机制。
我们只需要:
- 创建
neo-A
指向 与 branch-A
相同的 提交
- 运行
git rebase -i <hash of common starting point H>
- 对于我们不想要
的提交,将pick
更改为drop
和 Git 将自动倒回 neo-A
然后向前跳过或复制提交。最后的结果就是我们画的:
K'--L <-- branch-A
/
I--J--L' <-- neo-A (HEAD)
/
...--G--H <-- common-starting-point
\
K <-- branch-B
这是有效的,因为 Git 强制 name neo-A
在复制完成后移动到最后复制的提交,在这种情况下, L'
.
这会在名称为 neo-A
的任何 other Git 中产生问题,但我们只是编造了它,所以没有其他 Git 有它。所以现在 git push
是安全的。我们将发送新的提交,比如 L'
,给另一个 Git,然后要求他们设置他们的名字 neo-A
指向链中的最后一个提交——L'
——就像我们的名字一样。
我们不需要一个新名字,只要没有人参与
如果我们愿意,我们可以 git rebase -i
直接用 branch-A
本身来做。结果将是:
K'--L [abandoned]
/
I--J--L' <-- branch-A (HEAD)
/
...--G--H <-- common-starting-point
\
K <-- branch-B
然后我们可以使用 git push --force
或等效于 要求 另一个 Git 放弃其提交 L
和 K'
并使 its 名称 branch-A
指向提交 L'
。假设他们服从——他们可以拒绝——我们现在让他们的branch-A
搬家了。
这里唯一真正的问题是一些讨厌的人可能已经将 他们的 branch-A
复制到另一个 Git 存储库。该人可能希望他们的 branch-A
副本仅在正常的向前添加新提交方向上移动。这就是使用不同分支名称的真正原因:避免混淆对分支名称有期望的人。
如果这里没有人可以混淆,或者如果所有其他人事先知道这可能会发生,请随时倒带并强制推送现有分支名称。
新名称方法还有一个优点
假设,在复制你的提交同时删除其他人的提交的过程中,你犯了一个错误。
如果您使用自己的名字 branch-A
执行此操作,则很难 找到 您的原始提交系列。 Git 有一些技巧 (git reflog
) 可以帮助解决这个问题,但它们更多地用于紧急情况而不是日常使用。我发现创建新名称然后进行变基要好得多。如果出现问题,我仍然有 old 名称,从中我可以轻松地 find 所有旧提交以正确的顺序。
对于可以强制推送的私有分支名称,我有时会稍微改变一下顺序。而不是:
git checkout neo-A branch-A
git rebase -i <start-point>
我愿意:
git branch branch-A.0
git rebase -i <start-point>
现在如果出现问题,我有名字 branch-A.0
来记住原始提交。有时我会保留多个 "old versions of branch":
git branch branch-A.1
git rebase -i ...
git branch branch-A.2
git rebase -i ...
直到我有 "right" 提交集合。每个带编号的名称都会跟踪 "right commits".
的每个连续近似值
我在同一分支上为一个模块共同开发了一些代码,同时我也在另一个分支上与同事的模块进行了集成。基本上我最终同时修改了两个模块的代码,因为我让这两个模块相互通信。现在我想将所有代码一起提交,但不希望将分支中的内容混为一谈。
- 假设我的模块是 "Apple",在分支 A 上开发
- 我同事的代码是"Banana",开发于Branch B
- 我想在分支 C 上集成 "Cocktail"
所以基本上我在 A 上进行了开发,从 B 中挑选了我想要的更改,并且已经进行了测试集成。现在我想分离提交,让分支 A 只包含 "Apple",分支 B 只包含 "Banana",分支 C 包含集成代码。我不想重命名分支,所以我已经通过 cherry-pick 将所有更改从分支 A 复制到 C。另外,我已经挑选了从分支 A 到 B 的相关更改。
现在剩下要做的就是从分支 A 中减去提交,这样就只剩下 "Apple" 了。我正在寻找最好的方法来做到这一点。我不相信 git revert
是正确的选择,我想我可以做一个交互式 rebase 并删除我不想要的 A 提交,但我正在寻找验证这是正确的方向,或者如果需要其他东西。
分支名称并不重要,除了对人类而言。 (你是人类吗?如果是的话,我们可能会遇到一些障碍。)
对 Git 重要的是 提交 。提交 是 存储库中的历史记录。每个提交都有一个唯一的哈希 ID,这些哈希 ID 是 Git 查找提交的方式(我们稍后会看到一个重要的例外)。散列 ID 又大又丑,看起来 随机,人类基本上不可能使用,这就是我们使用分支名称的原因。
每个提交都包含所有文件的完整快照(好吧,所有出现在该提交中的文件)。而且,每个提交都包含一些元数据——关于提交的信息——例如提交者和时间,以及对 Git 本身非常重要的直接父提交的原始哈希 ID。所以这让 Git 从 last 提交开始并向后工作。
也就是说,假设哈希为 H
的提交是某个链中的 last 提交,标记为 branch-A
之类的分支名称:
... <-F <-G <-H <--branch-A
name branch-A
保存链中最后一次提交的哈希 ID,即提交 H
。提交 H
本身持有早期提交 G
的哈希 ID,后者持有早期提交 F
的哈希 ID,依此类推。
诀窍是 name 包含 last 提交的哈希 ID。没有其他简单的方法可以找到最后一个!最后一个找到倒数第二个,找到倒数第三个,依此类推,但不只是人类需要这个名字:Git 也需要它,找到 最后提交。
So basically I did my development on A, cherry-picked the changes I wanted from B, and already did the tested integration.
当您使用 git cherry-pick
时,您是在告诉 Git 复制 (效果和一些元数据)提交。所以你和任何在 B 上工作的人都是从一些共同的起点开始的,比如提交 H:
I--J <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
您提到您使用 git cherry-pick
从其他分支 复制 一些提交。将 K
复制到一个新的提交,我将其称为 K'
以表明它与 K
的相似程度,给出:
I--J--K' <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
请注意 name branch-A
现在如何指向新的 last 提交,现在是 K'
.提交 K'
具有所有内容的完整快照,但是将 K'
与 J
进行比较,您会看到与比较 更改 相同=33=] 到 H
。 K'
的作者和日志消息也将与 H
的作者和日志消息匹配,除非您告诉 Git 更改它们。当然,K'
的父哈希是J
,而K
的父哈希是H
。
您也可以添加更多提交,就像您可能所做的那样:
I--J--K'--L <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
Now all that's left to do is subtract commits from branch A so that only "Apple" is left. I'm looking for the best way to do this.
不一定有最好的方法。但是 Git 非常适合 add 提交到一个分支,更不用说 remove 从一个分支提交。如果你确实想删除一个提交,你可以告诉 Git 强制 name 向后移动。不再是 last 的提交现在无法找到。例如,如果我们强制名称 branch-A
向后移动以指向提交 I
,我们将得到:
J--K'--L [abandoned]
/
I <-- branch-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
使用 Git 的常规提交查看工具,例如 git log
,我们不会 看到 J
和 K'
和 L
,所以看起来它们已经消失了。1 查看者从 last 提交开始,由分支名称找到,并向后工作。
无论如何,这里的大问题是 Git 是为 add 提交构建的。你可以让你自己的 Git 向后移动你自己的分支名称,例如使用 git reset
或 git branch -f
,但这不会使任何 other Git,您已向其发送提交,将 其 名称向后移动。
1如果我们让他们在这个 unreachable 状态停留足够长的时间,提交 J
和 K'
最终会被垃圾收集。 git gc
命令,它 Git 运行 时不时地自己执行,一般来说,一旦它们已经这样运行了至少 30 天,就会执行此操作——所以你至少得到 30天,再加上 Git 到 运行 git gc
需要多长时间才能改变你的想法并让他们回来。
一个简单的解决方案
处理此问题的一个简单方法是使用新名称。由于 Git 不关心名称,我们可以只调用它 neo-A
而不是 branch-A
。我们将 neo-A
设置为指向现有提交 H
,您最初从哪里开始:
I--J--K'--L <-- branch-A
/
...--G--H <-- common-starting-point, neo-A
\
K <-- branch-B
现在,对于 neo-A
,我们将 添加 提交,一次一个。我们查看现有的提交。 I
是我们喜欢的,所以我们可以复制它,或者直接使用它,因为它很好。让我们做后者——直接使用它——通过使 neo-a
朝 L
的方向前进一步,Git "likes" 使分支名称移动的方式。 (当然,向 L
的方向移动很重要,而不是 K
,但这很容易,因为我们处于控制之中。)
J--K'--L <-- branch-A
/
I <-- neo-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
我们 H
到 L
方向的下一个提交是提交 J
。那一个也很好:我们可以按原样使用名称 neo-A
将 J
前进一步,给出:
K'--L <-- branch-A
/
I--J <-- neo-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
K'
是个问题:我们不想要它。所以我们只是不在这里前进。不过我们确实想要 L
,现在我们 必须 复制它,因为现有的 L
指向 K'
,我们想要一个新的指向 K'
改为 J
。所以这次我们需要使用git cherry-pick
,产生:
K'--L <-- branch-A
/
I--J--L' <-- neo-A
/
...--G--H <-- common-starting-point
\
K <-- branch-B
我们会继续,但此时我们已经完成了所有提交,也就是说,我们完成了。
我们现在可以安全地 git push
neo-A
name。那么我们只需要说服大家停止使用旧名称即可。
让这件事更容易发生
上述方法的缺点是我们必须 "move the name forward" 一次一个步骤。如果我们可以创建 neo-A
并让我们的 Git 完成所有工作,那就太好了。事实证明,我们可以。 git rebase
命令拥有我们喜欢的所有机制。
我们只需要:
- 创建
neo-A
指向 与branch-A
相同的 提交
- 运行
git rebase -i <hash of common starting point H>
- 对于我们不想要 的提交,将
pick
更改为drop
和 Git 将自动倒回 neo-A
然后向前跳过或复制提交。最后的结果就是我们画的:
K'--L <-- branch-A
/
I--J--L' <-- neo-A (HEAD)
/
...--G--H <-- common-starting-point
\
K <-- branch-B
这是有效的,因为 Git 强制 name neo-A
在复制完成后移动到最后复制的提交,在这种情况下, L'
.
这会在名称为 neo-A
的任何 other Git 中产生问题,但我们只是编造了它,所以没有其他 Git 有它。所以现在 git push
是安全的。我们将发送新的提交,比如 L'
,给另一个 Git,然后要求他们设置他们的名字 neo-A
指向链中的最后一个提交——L'
——就像我们的名字一样。
我们不需要一个新名字,只要没有人参与
如果我们愿意,我们可以 git rebase -i
直接用 branch-A
本身来做。结果将是:
K'--L [abandoned]
/
I--J--L' <-- branch-A (HEAD)
/
...--G--H <-- common-starting-point
\
K <-- branch-B
然后我们可以使用 git push --force
或等效于 要求 另一个 Git 放弃其提交 L
和 K'
并使 its 名称 branch-A
指向提交 L'
。假设他们服从——他们可以拒绝——我们现在让他们的branch-A
搬家了。
这里唯一真正的问题是一些讨厌的人可能已经将 他们的 branch-A
复制到另一个 Git 存储库。该人可能希望他们的 branch-A
副本仅在正常的向前添加新提交方向上移动。这就是使用不同分支名称的真正原因:避免混淆对分支名称有期望的人。
如果这里没有人可以混淆,或者如果所有其他人事先知道这可能会发生,请随时倒带并强制推送现有分支名称。
新名称方法还有一个优点
假设,在复制你的提交同时删除其他人的提交的过程中,你犯了一个错误。
如果您使用自己的名字 branch-A
执行此操作,则很难 找到 您的原始提交系列。 Git 有一些技巧 (git reflog
) 可以帮助解决这个问题,但它们更多地用于紧急情况而不是日常使用。我发现创建新名称然后进行变基要好得多。如果出现问题,我仍然有 old 名称,从中我可以轻松地 find 所有旧提交以正确的顺序。
对于可以强制推送的私有分支名称,我有时会稍微改变一下顺序。而不是:
git checkout neo-A branch-A
git rebase -i <start-point>
我愿意:
git branch branch-A.0
git rebase -i <start-point>
现在如果出现问题,我有名字 branch-A.0
来记住原始提交。有时我会保留多个 "old versions of branch":
git branch branch-A.1
git rebase -i ...
git branch branch-A.2
git rebase -i ...
直到我有 "right" 提交集合。每个带编号的名称都会跟踪 "right commits".
的每个连续近似值