从特定提交开始创建新分支

Create new branch from specific commit onwards

我想创建一个新分支,从某个提交开始。但是,我希望之后提交的所有提交也成为该分支的一部分。

让我们假设这是 branch-D 所有提交。

> branch-D
commit-A ---- commit-B ---- commit-C ---- commit-D

我想删除从 commit-B 到新的 branch 的所有内容,让我们将其命名为 branch-C,并且只在 branch-D[= 上保留 commit-A 18=]

> branch-D
commit-A

> branch-C
commit-B ---- commit-C ---- commit-D

我还没有将任何内容推送到远程存储库。 有办法吗?

最直接的方法是在 commit-A 之前的提交中创建 2 个新分支。然后 cherry-pick commit-A 到 1 个分支,cherry-pick 将 B、C、D 提交到第二个分支。

更新 - 我误读了您的图表,结果混淆了分支名称。同样正如 torek 指出的那样,"more correct" 阅读您的图表包含一些歧义。因为我相信它传达了主要原则,所以我将大部分保持原样(但将分支名称理顺);如果您需要进一步编辑 branchC 历史记录,我会稍微介绍一下使用变基;但要获得更详细的答案,请参阅 torek 的回复。


首先让我了解一下我认为您正在寻找的解决方案;但我建议阅读更多内容,因为这个问题中存在概念上的混淆,值得澄清。

所以你现在有

A -- B -- C -- D <--(branchD)

我认为你希望在 branchD 上以 A 结束,并为 B、[=21 创建一个新分支=],以及 D。所以第一步是,选中branchD(或D),创建新分支

git branch branchC

然后将 branchD 分支移回 A

git reset --hard HEAD~3

(我使用 HEAD~3 因为在这个例子中,那将是 A 的一个名称。它取决于从 master 的历史记录中取出的提交数量。使用 A 的提交 ID (SHA) 代替 HEAD~3 总是可以的。)

完成这些步骤后,您已经

A <--(branchD)
 \
  B -- C -- D <--(branchC)

看起来像你所描述的,但我们没有按照描述暗示的方式到达那里。

我没有移动提交;我搬了树枝。这在 git 中要简单得多,它反映出提交不会像在某些系统中那样 "belong to" 在 git 中分支。因此,语句 "branch at a previous commit but include the following commits on that branch" 在 git.

真的 没有意义

问题前后图表中的歧义确实让我有可能误解了 branchC 应该包含在其历史中的确切内容。如果 branchC 而不是 意味着包含 A,那么您必须重写 BCD - 您可以使用 git rebase -i 轻松做到这一点。创建 branchC 并移动 branchD 后,您可以说

git rebase -i branchD^ branchC

此处branchD^是“A之前的提交”的可能名称。如果有这样的提交,它可能有其他名称 - 也许master,或者肯定是它的提交ID。如果有没有这样的提交,那么我想你可以说

git rebase -i --root branchC

(但在那种情况下,尝试从 branchC 历史记录中删除 A 可能没有任何意义,所以我怀疑这就是这里发生的事情)。

rebase 命令将调出一个带有 TODO 列表的文本编辑器,每个提交都有一个条目。您可以从列表中删除提交 A 的条目,然后保存并退出。这不会打扰 branchD - 它仍将指向 A - 但它会通过复制 BCbranchC 创建新的历史记录D。那么你会有

x -- B' -- C' -- D' <--(branchC)
  \
   A <--(branchD)

无论如何,做你想做的事意味着从 branchD 的历史记录中删除 BCD。你提到这些提交还没有被 pushed,所以应该没问题;但这是一个需要牢记的重要区别。任何时候你想从分支的历史记录中删除提交,如果远程分支还不知道有问题的提交,那么这样做会更容易。

关于 Git 的特殊之处在于分支 names 不会影响你的 commit history 中的内容,除了他们让你——以及其他任何人,这非常重要——找到 提交。秘诀在于,在 Git 中,一个提交可能同时在 多个 分支 .

但重要的是,任何提交一旦完成,就无法更改。它可以复制到(新的,略有不同的)提交,但不能更改。

此外,虽然任何分支名称 可以 被强制指向任何提交,但有一个 "normal direction" 用于分支名称运动:通常,它们只 前进。正如我们稍后将看到的那样,前进意味着他们过去指向的任何提交仍然是 "on" 那个分支。因此,一旦您将您的提交交给其他人——其他 Git——并要求他们调用这些提交 branch-D,就很难获得 其他 Git 收回那个。因此:

I haven't pushed anything to a remote repository yet. Is there a way to do this?

是的,可能有一个非常简单的方法:"haven't pushed anything yet" 意味着您是唯一一个 拥有这些提交的人,所以无论您如何找到它们,这就是 每个人 找到它们的方式,因为你是 "everyone"。 :-) 无需让其他人更改任何内容。

只要它们已经按您想要的方式布置,您只需重新安排您找到这些提交的方式。

让我们绘制您现有的一系列提交 "Git Way":

... <--A <--B <--C <--D   <-- branch-D

在 Git 中,每个提交都由其哈希 ID 唯一标识。这些东西又大又丑而且显然是随机的(尽管它们实际上是完全确定的),所以我们几乎从不使用它们,或者使用像 d1c9d3a 这样的缩写形式。取而代之的是,我们只调用最后一次提交 D.

在提交 D 内,还有另一个哈希 ID,标识 D 父提交 。假设它是 c033bae,但我们就称它为 "commit C"。所以我们说D指向C.

类似地,C 指向 B,后者指向 A,后者指向......好吧,也许是 master 的提示提交—你没有说,但我们现在假设:

...--o--o   <-- master
         \
          A--B--C--D   <-- branch-D

这是一种更紧凑的绘制方式。总是,必然地,指向向后,所以我们真的不需要内部箭头——我们知道它们总是向后的。分支 names,但是,像 masterbranch-D ......好吧,我们可以在 anywhere 上创建这些点。我们要做的是让它们指向一个分支的"tip commit"。

Git 从分支名称指向的那个开始查找提交:D,对于 branch-D,或者第一行的最后一个 o master。然后它查看当前提交的父级,向后移动。然后它查看父对象的父对象,等等。因此,两个 o 提交都在 master branch-D 上:我们可以从 master 开始找到它们,或者我们可以从 branch-D 开始并向后四步找到它们。

这意味着我们想要的图片看起来可能是这样的:

...--o--o   <-- master
         \
          A   <-- branch-D
           \
            B--C--D   <-- branch-C

在这里,master 上的提交也在两个分支上,提交 A,现在是 branch-D 的尖端,仍在 [=50= 】 还有。它不再是 branch-C 提示


另一方面,也许我们想要的图片是这样的:

          ?--?--?   <-- branch-C
         /
...--o--o   <-- master
         \
          A   <-- branch-D
           \
            B--C--D   <-- ???

也就是说,我们需要回答一个问题。名称 branch-C 将指向某个特定的提交。当我们倒退三步时,我们应该到达提交 A 吗?或者我们应该到达 master 的最后一次提交?


如果第一张图是对的,答案很简单:重新命名,branch-C,指向提交D;然后强制现有名称 branch-D 移回提交 A。为此:

git branch branch-C branch-D  # copy the hash ID from branch-D to new branch-C

然后,根据当前签出的分支,可以:

git reset --hard <hash-of-A> # move current branch; re-set index and work-tree

或:

git branch -f branch-D <hash-of-A> # force branch-D to point to A

请注意,在使用 git reset --hard 之前,最好确保没有任何要保存的修改。虽然 commits 几乎是永久性的(您可以将它们取回,通常至少 30 天,即使您将它们从所有分支名称中剔除),索引和工作树 git reset --hard 破坏者 不是

但是,如果您想要第二张图片——如果您希望提交 B 的父级不是提交 A——您将不得不 复制 提交 B 到一个新的、不同的提交,即 "like B, but..."。原始 B 和副本之间的差异将包括更改的父哈希 ID。

对于 that,您将需要使用 git cherry-pick 或等价物(例如 git rebase,这基本上是一个 en-masse cherry-pick复制大量提交的操作)。为此,您可以:

git checkout -b branch-C master

给予:

...--o--o   <-- branch-C (HEAD), master
         \
          A--B--C--D   <-- branch-D

然后运行三个git cherry-pick命令复制BCD;或更简单——这使用 <commit>~<number> 表示法:

 git cherry-pick branch-D~3..branch-D

一次复制所有三个。这会产生:

          B'-C'-D'   <-- branch-C (HEAD)
         /
...--o--o   <-- master
         \
          A--B--C--D   <-- branch-D

此时,强制 branch-D 指向提交 A:

是安全的
git branch -f branch-D branch-D~3

您也可以像前面的示例一样通过哈希 ID 来执行此操作:

git branch -f branch-D <hash-of-A>

我们对 branch-D~3 所做的一切就是告诉 Git:倒数三个父步骤,一次一个父步骤。所以我们从 D 开始,倒数一步到 C,再倒数一步到 B,再倒数第三步到 A

您可以使用:

git checkout hashOfCommitA

然后

git checkout -b NewBranchName

现在你有新的分支提交 A。然后你可以返回提交 D

git checkout nameOfFirstBranch

然后从此分支还原提交 A

git revert hashOfCommitA

现在你必须在其中一个分支上只有提交 A,而在另一个上有提交 b、c 和 d。