Git 将不同分支中的旧提交推送到新分支

Git pushes old commit in different branch to new branch

我一直在为 GIT 苦苦挣扎,因为它做了一些奇怪的事情。

我有一个包含初始代码的基本提交。

好的,接下来我创建一个分支来进行编辑、提交和推送:

git checkout -b feature/feature1
git commit -m "added feature1"
git push origin feature/feature1

现在我对该功能提出拉取请求。

之后我更改分支并更新例如:

git checkout -b chore/update-framework
git commit -m "updated framework"
git push origin chore/update-framework

此时,当我在第二次推送时尝试拉取请求时,它包含第一个提交,其中包含所有功能。因此,该提交中所有更改的文件都在新的拉取请求中,而旧的拉取请求仅包含功能文件。

我做错了什么?

当你这样做的时候:

git checkout -b chore/update-framework

在 .

之前,您没有在 master 上结帐

从你想要的提交开始一个新分支(以 master 为例):

git checkout -b chore/update-framework master

或:

git checkout master
git checkout -b chore/update-framework

或:

git branch chore/update-framework master
git checkout chore/update-framework

要纠正你的错误,你可以这样做:

git rebase --onto master feature/feature1 chore/update-framework

Git 将选择 chore/update-framework 的所有未被 feature/feature1 指向的提交,然后将它们放入 master 并最终更新您的 chore/update-framework 分支。

我认为您的问题归结为了解提交在(或者说 包含在)分支上意味着什么。这在 Git 中不同于许多传统版本控制系统。

在Git中,每个提交都在(或包含在)许多个分支上,至少有可能。它可以在无分支、1 个分支、2 个分支等上。这是因为,在 Git 中,分支 name 只是指向 一个特定提交 的可移动指针。把它想象成一张黄色的便利贴。创建一个新的分支名称意味着抓起一张空白的便利贴并在上面写下名称,feature/feature1.

但是附在哪儿的便条呢?好吧,在这里绘制(部分)提交 graph.

有很大帮助

绘制(部分)提交图

在 Git 中,每次提交 "points to" 其前任(或 parent 提交)。对于线性提交链,这基本上是一系列及时向后指向的箭头:

... <- o <- o <- o <- ... <- o

这里的每个 o 代表一个提交,每个提交指向它的(一个)parent.

合并提交指向 two parents,这实际上是合并提交的原因。当在 Whosebug 上限制为纯文本时,用箭头绘制要困难得多:

           o--o
          /    \
... --o--*      M--o
          \    /
           o--o

想象所有线条上的箭头,它们都指向 left-ish(up-and-left,或 down-and-left,如果需要)。 M 这是合并提交,有两个 parent。我在这里标记了一个提交,commit *。它 不是 合并提交,但它确实有两个 children (两个提交指向它)。这有时会使它变得更有趣。

事实上,提交 * 特别有趣 在我们进行合并提交之前 。让我们绘制它(通过擦除 M 和一些 os):

           o
          /
... --o--*
          \
           o--o

在绘图中添加分支名称

这是分支真正成为焦点的地方。让我们添加名称和更长的 <-- 箭头:

           o   <-- bra1
          /
... --o--*
          \
           o--o   <-- bra2

分支 bra1 指向(或者,等价地,是粘贴黄色便利贴)上层 tip-most o 提交,bra2 指向下层tip-most o.

我们称为 * 的那个提交在两个分支上都是 (就像它左边的每个提交一样)。

这个特殊的谜题还有两块。一个是名称 HEAD,另一个与我们添加新提交和编写新分支名称时发生的事情有关。

HEAD

HEAD 在 Git 中的名称告诉我们当前提交和当前分支。 方式 Git 做这件事几乎简单得可笑:HEAD 通常只包含分支的名称。把它想象成另一个便利贴(可能是亮粉色、绿色或紫色,只是为了让它明显有点不同)。它 可以 直接指向一个提交——那是你肯定见过的 "detached HEAD" 东西——但通常它只是写了一个分支名称:

           o   <-- HEAD->bra1
          /
... --o--*
          \
           o--o   <-- bra2

这意味着我们在分支 bra1

发展分支

让我们在 bra1 时进行新的提交。

进行新提交时,Git:

  1. 读取 HEAD(它表示 branch bra1)。
  2. 从分支 bra1 读取提交 ID(这是一些丑陋的 SHA-1 a1c397f... 或其他)。
  3. 使用您 git add 编辑的任何内容进行新提交,并在您的日志消息中指向该 parent ID。新提交获得一个新的唯一 SHA-1(例如 0bc3112...)。
  4. 将这个新数字写入当前分支,bra1

第 4 步导致 bra1 指向新的提交,现在我们有:

           o--o   <-- HEAD->bra1
          /
... --o--*
          \
           o--o   <-- bra2

添加合并提交

为了完整起见,让我们看看进行合并提交。

进行合并的过程本身可能很混乱,但实际上进行合并 提交 很简单:它只是一个有两个 parent 的提交。请注意,我们仍在 bra1。我们运行git merge bra2。 Git 启动合并机制来完成 work-tree 中的工作。如果有冲突,它会留下一团糟,我们必须手动修复它,但如果没有,它会自动开始新的提交。

新的提交与以前一样,只有一个小的变化。在第 3 步中,不是写 one parent ID(来自现有 bra1 的那个),而是写 two parent IDs:第一个是通常的ID,第二个是读取得到的ID bra2.

第 4 步照常工作,将新提交的 ID 写入 bra1(因为 HEAD 仍然说 "use branch bra1"):

           o--o
          /    \
... --o--*      M   <-- HEAD->bra1
          \    /
           o--o   <-- bra2

因为 M 是一个合并(有两个 parent),这意味着所有以前独占 bra2 的提交现在都在 bra1还有!

提交 * 曾经在两个分支上,现在仍然是。和,我们仍然可以通过从 bra2 开始并向左工作来获得那两个 post-* 提交。

我们只允许向左移动(大概),所以从 bra2 开始,我们 不允许 移动到 M,这意味着我们无法到达第一行提交。我们只能从那里开始,或者从 M 开始。然而,我们不仅被允许,而且实际上 需要 ,遵循 所有 合并的 parent,例如 [=26] =].所以从bra1开始,我们得到M,然后我们得到分支结构的两边,一直回到提交*,并从那里继续向左

如果计算节点数,您会看到 bra1 中现在包含三个提交,bra2 中不包含这些提交...但是 bra2 中包含所有提交包含在 bra1.

好的,那么创建个分支呢?

这里 HEAD 重新发挥作用。

您可以使用git branch只需创建一个分支:

$ git branch bra3

默认情况下,这会显示 HEAD 来确定我们现在所在的位置。 HEADbra1 并且 bra1 里面有 M 的 ID。所以这使得 bra3 指向 M:

           o--o
          /    \
... --o--*      M   <-- HEAD->bra1, bra3
          \    /
           o--o   <-- bra2

请注意,HEAD 仍然指向原来的位置,也没有其他分支受到干扰,我们只是添加了一个新的可移动标签 bra3。如果我们现在移动 bra1bra2bra3 继续指向 M

由于 HEAD 仍然指向 bra1,新的提交将进行 bra1 更改。

但是,如果我们使用 git checkout -b 创建新分支,我们会得到这个:

           o--o
          /    \
... --o--*      M   <-- bra1, HEAD->bra3
          \    /
           o--o   <-- bra2

这看起来几乎一模一样。不同的是,除了添加新的分支名称外,我们更改了HEAD。现在它指向 bra3.

创建一个指向当前提交以外的内容的新分支

让我们回到git branch,只做分支,不改变HEADgit branch 命令还有一个可选参数:

$ git branch bra4 bra2

不是让 bra4 指向与 HEAD 相同的提交,而是 "make bra4 point to the same commit as bra2"。所以现在我们有:

           o--o
          /    \
... --o--*      M   <-- bra1, HEAD->bra3
          \    /
           o--o   <-- bra2, bra4

现在让我们git checkout bra4,看看它做了什么(就图表而言——当然,它也检查文件):

$ git checkout bra4

           o--o
          /    \
... --o--*      M   <-- bra1, bra3
          \    /
           o--o   <-- bra2, HEAD->bra4

同样,任何分支标签本身都没有发生任何变化。在图表中,我们只更改 HEAD 点的位置。

(我们本可以使用组合形式——我们现在已经有了 bra4,所以现在为时已晚,但它本来可以是一个命令而不是两个命令——通过执行 git checkout -b bra4 bra2。)

新分支指针的底线

这意味着我们可以选择提交一个新分支指向每当我们创建新分支时。 默认的是"wherever HEAD points",这通常意味着读取另一个分支名称。我们可以先让 HEAD 指向某个有用的地方,或者我们可以将其添加到 branch-creator 并让它们一起发生。

还有一个问题,它并不总是只有一个正确答案:我们应该将那个新分支指向哪里?

可能我们想将它指向 origin/masterorigin/feature1 之类的东西。有时选择其他起点更有意义,甚至可能没有标签指向它。

等等,origin/master是一个标签?

是的,这些东西也是分支标签——指向提交的可移动指针。它们只是不是 移动的标签。当您 运行 git fetch (或拉动,但拉动只是获取,然后是另一步)时,您让它们移动。换句话说,他们 "track the remote"(你 git fetch originorigin/* 如果需要就移动);所以它们是 remote-tracking 个分支 .

当我们绘制上面的图表时,我们可能应该包括这些标签......也许是这样的:

           o      <-- HEAD->feature1
          /
... --o--*        <-- origin/feature1
          \
           o--o   <-- feature2

如果你想制作一个新的 feature3 也从 * 中长出来,git checkout -b feature3 origin/feature1 可以做到。提交 * 将包含在 四个 分支中:feature1origin/feature1feature2feature3