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

git | move old commit to the past of another branch

我过去错误地分支了,一个提交留在了另一个分支的开头:

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

如何将 a1foo 移到 bar,以便 bar 的历史记录为 root -> a1 -> a2a1不在foo?是否可以通过一次 git 提交来完成?
这不是推送所以不用担心破坏别人的本地回购协议。

我首先想到的是对 a1 做一个 cherry pick,然后更正 a2a1 之间的顺序。这样做的问题是,在我的真实案例场景中,这两个提交发生冲突,我必须在进行樱桃挑选和切换顺序时纠正冲突。


mwe 在 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

我非常喜欢语法 git rebase --onto x y z,这意味着:

Starting at z, look back thru the parent chain until you are about to come to y and stop. Now rebase those commits, ie everything after y up to and including z, onto 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. 现在您可以根据需要删除临时文件: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

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

Is it possible to do [what I want] with one single git commit?

我假设你在这里指的是一个Git命令而不是一个Git提交。答案是:不,不可能那样做。

How can I move a1 out of foo into bar, so that bar's history is root -> a1 -> a2 and a1 is not in foo?

有关使用 git rebase --onto 执行此操作的方法,请参阅

但是,按照 Git 的看法可能会有所帮助。这不是 root -> a1 -> a2。是 root <-a1 <-a2。名称 foo 本身可以移动,但这里的三个现有提交是一成不变的:它们的任何部分都不能更改。没关系,因为正如您所说,这些提交中的 none 已发送到其他任何地方。

无论你做什么,你都必须复制 一些提交到new-and-improved 提交,使用不同的哈希ID。您可以单独保留现有的 carved-in-stone 提交,前提是您要更改 nothing,包括作为提交一部分的 backwards-pointing 箭头。

root 提交(实际上:6ebca20)没有 backwards-pointing 箭头。这就是使它成为根提交的原因。只要您不喜欢这个提交的任何内容,您就可以不理会它——这很好,因为 root 提交很难复制。 git cherry-pickgit rebase 都可以 做到这一点,但这有点奇怪 special-case-y,因为 cherry-pick 或 rebase 操作有效通过将提交的快照与其 parent 的快照进行比较。 no parent 根提交的事实让它有点奇怪。1

提交 a1(实际上:4656a98)向后指向根提交。好像也可以。

提交 b1,然而——这实际上是 9b29ae3——向后指向提交 4656a98。这是一成不变的。它不能改变。您可以创建一个新的不同的提交,您也将其称为 b1,而不是向后指向根提交,这就是您想要做的。

使用 b1 完成此操作后,您现在需要将提交 b2 复制到 new-and-improved 提交,因为现有的 b2 (d332e4d)指向 9b29ae3。您需要一个进行相同更改的副本 — cherry-pick 会为您做些什么 — 但它有新的 b1,无论它的哈希 ID 变成什么,因为它的 parent.

复制 b1b2 后,您可以将名称 foo 指向 b2 的副本。 Git 中的分支名称只是 指向 一些实际的现有提交。您可以随时更改它们指向的提交;他们指向 的任何提交都是 分支中的最后一次提交。通过 last 提交中的 parent 链接向后工作,然后向后工作另一个步骤到 parent 的 parent(或 parents' parents 或其他)。

由于您必须b1 复制到new-and-improved 版本,这会强制您也复制b2git rebase 命令 运行s git cherry-pick 重复完成上述复制,然后——一旦所有复制完成——移动 分支名称 指向最后复制的提交。所以一个 git rebase,具有正确的选项(包括 --onto),将完成 foo 分支的技巧。

另外,您必须将现有的a2复制到new-and-improved版本。完成复制后,您必须移动 name bar 以指向复制的 a2。一个 git rebase 命令也足以完成所有这些操作。

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


1cherry-pick 代码本身通过使用 faked-up“没有文件”parent 暂时处理这个问题,因此就 cherry-pick 操作而言,提交所做的“更改”是 添加所有文件 。 rebase 代码需要 --root 选项,至少在 Git 的旧版本中是这样;我不确定这里发生了什么,因为音序器现在已经学会了做以前在 shell 脚本中的交互内容。