git | 将旧提交移动到另一个分支的过去

3

我之前曾错误地创建了一个分支,结果有一个提交留在了另一个分支的起点:

* 03431cb (HEAD -> bar) a2
| * d332e4d (foo) b2
| * 9b29ae3 b1
| * 4656a98 a1
|/  
* 6ebca20 (master) root

如何将代码库中的 a1foo 移到 bar,以便 bar 的历史记录是 root -> a1 -> a2,并且 a1 不在 foo 中?是否可以使用一个 git 提交完成此操作?
由于未推送,因此无需担心破坏其他人的本地代码库。
我最初考虑了对 a1 进行 cherry-pick,然后在 a2a1 之间更正顺序。但问题在于,在我的实际情况下,这两个提交会产生冲突,并且在执行 cherry-pick 和调整它们的顺序时都需要解决冲突。
Bash 中的最小可复现示例:
#!/bin/bash
set -e

rm -rf .git

git init -b master
echo content > my-file
git add my-file
git commit -m root

git checkout -B foo
echo asd >> my-file
git add my-file
git commit -m a1
echo qwe >> my-file
git add my-file
git commit -m b1
echo zxc >> my-file
git add my-file
git commit -m b2

git checkout master
git checkout -B bar
echo jkl >> my-file
git add my-file
git commit -m a2
2个回答

7
我非常喜欢使用语法git rebase --onto x y z,它的意思是:

从z开始,向上回溯父节点链,直到即将到达y为止并停止。现在将这些提交(即y后面的所有内容,包括z)变基到x上。

换句话说,使用这种语法,您可以清楚地指定在哪里剪切链。而且,在变基之前不必切换分支。这种语法需要一些时间来适应,但一旦熟练掌握,您会发现自己经常使用它。
所以:
  1. 创建一个临时分支a1仅用于命名:git branch temp 4656a98
  2. 现在将b1和b2变基到根:git rebase --onto master temp foo
  3. 最后将a2变基到a1:git rebase --onto temp master bar
  4. 现在如果愿意,可以删除temp了:git branch -D temp
当然,我们可以通过SHA编号4656a98而不是名称temp来完成2和3,但名称更好记。

证明。

起始位置:

* 9a97622 (HEAD -> bar) a2
| * 83638ec (foo) b2
| * 7e7cbd0 b1
| * 931632a a1
|/  
* 6976e30 (master) root

现在:

% git branch temp 931632a
% git rebase --onto master temp foo
% git rebase --onto temp master bar
% git branch -D temp

结果:

* 3a87b61 (HEAD -> bar) a2
* 931632a a1
| * bbb83d0 (foo) b2
| * 5fa70af b1
|/  
* 6976e30 (master) root

我相信这就是你所说的想要的内容。


2

我能用一个git命令完成我想要的操作吗?

我认为你指的是一个Git命令而不是一个Git提交。答案是:不,这是不可能的。

我该如何将 a1foo 移到 bar,使得 bar 的历史记录是 root -> a1 -> a2 并且 a1 不在 foo 中?

请参见matt的回答以使用 git rebase --onto 进行操作。

然而,了解Git的视角可能会有所帮助。它不是 root -> a1 -> a2。它是 root <-a1 <-a2。名称 foo 本身可以移动,但这里的三个现有提交已经定型:它们的任何部分都不能更改。这没关系,因为正如你所说,这些提交没有被发送到其他地方。

无论你做什么,都必须将某些提交复制到新的和改进的提交中,并具有不同的哈希值。只要你不改变它们中的任何内容,包括作为提交一部分的向后指向箭头,就可以保留现有的定型提交。

根提交(实际上是: 6ebca20)没有向后指向的箭头。这就是它成为根提交的原因。只要你不讨厌这个提交中的任何内容,你就可以将其保留下来——这很好,因为根提交很难复制。无论是使用 git cherry-pick 还是 git rebase 都可以做到这一点,但所有这些都有点奇怪和特殊,因为樱桃拣选或重置操作通过比较提交的快照与其父提交的快照来工作。根提交没有父提交的事实使得它有点奇怪。1

提交 a1(实际上是:4656a98)向后指向根提交。这也是可以接受的。

然而,提交 b1——实际上是 9b29ae3——向后指向提交 4656a98。这是定型的。它不能被更改。你可以创建一个新的、不同的提交,也称为 b1,它向后指向根提交,这就是你想要做的。

完成了对的处理后,你现在需要将提交复制到一个新的、改进的提交中,因为现有的()指向9b29ae3。你需要一个副本,它做出相同的更改——cherry-pick可以为你完成,但是它的新父级是新的,无论其哈希ID变成什么。

一旦你复制了和,你就可以将名称指向的副本。在Git中,分支名称只是指向某个实际存在的提交。你可以随时更改它们所指向的提交;它们所指向的提交是该分支中的最后一个提交。通过在最后一个提交中向后工作以通过父链接向后工作另一步来查看较早的提交,先前的提交也是在分支中的。

由于你必须将复制到新的、改进的版本中,这迫使你也要复制。git rebase命令重复运行git cherry-pick来实现所述复制,然后——一旦所有复制都完成——将分支名称移动到指向最后复制的提交。因此,一个git rebase,配合正确的选项(包括--onto),就可以为foo分支完成任务。

另外,你必须将现有的复制到一个新的、改进的版本中。完成复制后,你必须将名称移动到已复制的上。一个git rebase命令就足以完成所有这些操作。

因为git rebase可以为你运行git checkout,所以在这一点上需要的最少的Git命令是两个。这导致了matt使用的命令集:额外的两个命令只是为了方便。


1cherry-pick代码本身通过临时使用一个伪造的“没有文件”的父级来处理这个问题,因此就像cherry-pick操作所涉及的那样,“更改”由提交完成——添加所有文件。重写代码要求至少在较旧版本的Git中使用--root选项;我不确定自从序列器现在已经学会执行曾经在shell脚本中的交互式操作后,在这里发生了什么。


很有见地。特别是“foo本身的名称可以更改,但这里的三个现有提交是铭刻在石头上的:它们的任何部分都不能更改”对我来说还不太清楚。 - ffigari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接