首页 > 解决方案 > 如何从子分支直接提交到父分支?

问题描述

假设我在基于分支 A 的分支 B 中。我对在分支 B 上完成的提交感到满意,我想将该提交“推送”到分支 A 并以此为基础。此外,我仍在处理分支 B 上的许多未提交的文件。我可以执行以下操作:

<B> git commit -m "msg"
<B> git stash 
<A> git checkout A
<A> git cherry-pick my_commit_hash_from_branch_b
<A> git checkout B
<B> git rebase A
<B> git stash pop

如果分支尚未分叉,是否有从分支 B (直接提交到父分支)执行上述操作的捷径?

标签: git

解决方案


分支在 Git 中没有父母,所以这个问题不能完全按照措辞来回答。但我想我知道你的意思。它既比您想象的要难(在某些情况下,您不是在考虑),也比您想象的要容易。而且,如果您的 Git 至少为 2.15 或更高版本,我建议git worktree add您出于理智的目的进行调查。

背景

让我们画出这个,并确保我们都同意这一切的含义。我们从提交有父节点的事实开始,所以如果我们绘制一个简单的线性提交链,使用大写字母代表实际的哈希 ID,我们会得到这样一张图片:

... <-F <-G <-H

whereH代表某个提交的哈希 ID,它是某个分支上的最后一次提交。提交H由两部分组成:所有文件的源快照和一些元数据。在提交的元数据中H,Git 存储了先前提交的实际哈希 ID G,因此H指向G. 提交G是类似的:它存储一个快照和一些元数据,并且元数据导致G向后指向F,它本身向后指向。这个向后看的提交链存储库中的历史记录。

分支名称出现在这张图片中的一点是,它们让我们(和 Git)通过存储每个“最后一次提交”的哈希 ID 来找到提交。因此,如果我们有分支br1br2- 两个不同的分支名称 -具有H,或者更确切地说是 的完整大丑哈希 ID H,在它们中,我们有这样的图片:

...--F--G--H   <-- br1, br2

每当我们进行提交时,Git 都会将新提交写出——它会写入快照和一些元数据——并将新提交的父哈希设置为当前提交。写出新的提交会为其分配新的、大而丑陋的随机哈希 ID,我们将其称为I,因此它I指向H

...--G--H
         \
          I

然后最后一步git commit是将I'hash ID'写入当前分支名

为了完成所有这些工作,Git 需要知道两件事:

  1. 当前分支的名称是什么?
  2. 当前提交的哈希 ID 是多少?

Git通过将该名称附加到两个分支名称之一中,从特殊名称中获取这两件事HEAD所以在我们的初始状态:

...--G--H   <-- br1, br2

我们真的有:

...--G--H   <-- br1 (HEAD), br2

通过询问 Git:附加到哪个分支名称?HEAD我们找出我们正在使用的分支名称,当然那是br1. 通过询问 Git:名称找到了哪个哈希 ID ?HEAD我们找出我们正在使用的提交,那就是commitH。如果我们检查另一个名称br2,我们更改名称而不更改提交

一旦我们进行了的提交,两个分支上的提交仍然两个分支上,但是已经推进的一个分支名称现在仅在该分支上具有一个提交:

...--G--H   <-- br2
         \
          I   <-- br1 (HEAD)

你从字面上问的(这不是你要问的)

假设我在基于分支 A 的分支 B 中。

让我们画这个。由于我使用 A、B、C 之类的字母进行提交,因此我将在这里使用branch-Abranch-B

...--G--H   <-- branch-A
         \
          I--J--K   <-- branch-B (HEAD)

我对在分支 B 上完成的提交感到满意,我想将该提交“推送”到分支 A 并以此为基础。

您希望包含I-J-K当前仅在的三个提交中的哪一个?在不进行任何新提交的情况下,此时您唯一的选择是将标签移动到指向、或。如果你选择指向它,你会得到:branch-Bbranch-A branch-AIJKI

...--G--H--I   <-- branch-A
            \
             J--K   <-- branch-B (HEAD)

这实现了你想要的,但如果你选择指向它,J你会得到:

...--G--H--I--J   <-- branch-A
               \
                K   <-- branch-B (HEAD)

这有效地将提交I 移动 Jbranch-A.

此外,我仍在处理分支 B 上的许多未提交的文件。

未提交的文件永远不会在任何分支上!

分支名称从不包含任何文件。一个分支名称包含一个提交哈希 ID。根据定义,该一次提交是分支上的最后一次提交。这就是我们在上面看到的。因此,如果某些内容未提交,则它不能在任何分支上。只有提交在分支上。

考虑到这一点,这就是我认为您要问的问题

您想到的设置不是:

...--G--H   <-- branch-A
         \
          I--J--K   <-- branch-B (HEAD)

反而:

...--G--H   <-- branch-A, branch-B (HEAD)

其中 commitH当前提交,是两个分支上的最后一次提交。同时,您一直在工作并且有一些文件未暂存以进行提交和/或一些文件已暂存以进行提交(有关此内容的更多信息,请参见下面的可选“更多背景”)。此时,您可能希望进行新的提交,但将两个分支名称都提前,如下所示:I

git <mystery command set 1 (if any commands are required here)>
git add <arguments>
git commit <arguments>
git <mystery command set 2>

结果是:

...--G--H--I   <-- branch-A, branch-B (HEAD)

新提交I具有您选择提交的更新,并且您的工作树保持不受干扰。

这部分很容易做到!set 1 中的神秘命令根本不是命令,对于 set 2 中的神秘命令,有一种偷偷摸摸的方法来(ab?)使用git push作为单个命令来完成这项工作。这是我们所做的:

git add ...         # add any files you want updated in commit `I`
git commit          # use -m option if desired (rarely a good idea; see notes)
git push . branch-B:branch-A

git commit命令导致:

...--G--H   <-- branch-A
         \
          I   <-- branch-B (HEAD)

像往常一样,新提交I只在 branch branch-B。该git push命令是我们如何推进名称 branch-A的。我们使用特殊的“远程”名称.——它的特殊之处在于它实际上根本不是一个远程;它指的是这个存储库本身——实际上,我们调用了自己的 Git,就好像它是另一个 Git。我们的 Git 现在询问“其他 Git”(即我们自己)是否有提交I——我们当然有——然后请求“其他 Git”(即我们自己)更新“它的”(我们的)分支名称branch-A,在快进时尚,1这样名称现在指向branch-B. 这对我们自己当然没问题:推送被接受,我们自己的 Git 的branch-A标签现在也指提交I

...--G--H--I   <-- branch-A, branch-B (HEAD)

这正是我们想要的。(请注意,如果您愿意,您可以将git push命令拼写为 as git push . HEAD:branch-A;这种拼写适合在 Git 或 shell 别名中使用,因为它通过特殊名称的魔力自动引用当前分支HEAD。因此您只需指定您想要快进的名称。)


1请记住,当存储在某个名称(通常是分支名称或远程跟踪名称)中的新哈希 ID 表示的提交是其哈希 ID 在该名称中的提交的后代时,会发生快进前。也就是说,可以从 to 迁移到HI因为IhasH作为父级。也可以从F直接移动到I,因为I通向H哪个通向G哪个通向F


更多背景(全部可选)

了解 Git 在提交时使用的实际机制很重要。Git可以更好地隐藏这一点,但故意不这样做。因为它没有,Git 说的一些东西在你理解它之前是没有意义的。但在我们了解Git 在这里做什么之前,让我们从 Git 这样做的原因开始

每次提交中的所有内容都会一直冻结。这包括每个快照中的所有文件。从字面上看,它们是无法改变的。此外,它们采用特殊的仅 Git 格式,大多数程序无法读取。但是如果它们不能被改变,甚至不能被其他程序读取,你怎么能完成任何工作呢?

出于这个原因,Git 必须先从提交中复制所有文件然后才能使用它们。这意味着您使用的文件——Git称之为工作树工作树的文件——不在Git 中。

现在,Git可以停在这里,只有文件的当前提交冻结副本,以及您可以修改并放入新提交的有用副本。其他版本控制系统确实到此为止。但是在 Git 的情况下(以及在其他一些 VCS 中),这会使提交变得很慢:Git 必须重新压缩和重新 Git-ify 每个文件,看看它是相同的还是不同的, 等等。所以它不会那样做。

Git可以让您宣布您更改了哪些文件,并及时重新 Git 化这些文件git commit。但是 Git 也没有这样。相反,Git 所做的是在您运行每个更新的文件时对其进行重新 Git-ify git add。为了方便自己进行这项工作,Git 保留了一个具有三个名称的数据结构。这个东西的三个名称是索引暂存区缓存。如今,“缓存”这个名字已经很少见了:它主要出现在诸如git rm --cachedor之类的拼写中git diff --cached。名称暂存区是指您如何使用它。无意义的名字,索引, 是 Git 内部使用最多的一个,因为“暂存区”并没有涵盖 Git 大部分成功隐藏的一堆辅助用途。

这对你意味着什么——除了必须知道它的三个术语——你需要知道 Git 从 Git 的索引而不是你的工作树中进行的提交,因此你必须明确地将文件复制Git 的索引中。该索引始终保存将进入下一次提交的每个文件的副本。初始签出不仅将提交的文件提取到对您有用的形式中在您的工作树中。它还将相同的文件“提取”到 Git 的索引中,以对 Git 有用的形式:预压缩和 Git 化,准备进入下一次提交。因此,在任何时候,每个文件都有三个活动副本:一个在当前或HEAD提交,一个在 Git 的索引中,一个在你的工作树中。只是这些副本中的两个甚至三个都匹配是非常常见的。

Git 真的只是在git commit这里让事情变得简单。由于索引始终保存建议的下一次提交,因此git commit只需打包(已经冻结的)文件并在新提交中使用它们。但是这种方式需要做更多的工作git status,因为现在 Git 必须运行两个比较:

  • git status必须比较HEAD提交与索引(建议的下一次提交)。对于每个相同的文件,它什么都不说,但对于每个不同的文件,它都说staged for commit
  • git status还必须将索引与您的工作树进行比较。对于每个相同的文件,它什么也没说;对于每个不同的文件,它表示not staged for commit

Git 在这里隐藏了许多其他技巧,但无论如何,这些都是你必须牢记的。

请注意,索引也是 Git 如何知道快照中的文件的方式。新提交中的文件是当前在 Git 索引中的文件。如果你从 Git 的索引中删除一个文件——使用git rm,有或没有--cached——该文件现在不再在提议的下一次提交中。如果您使用 将一个全新的文件添加到 Git 的索引中git add,则该文件现在位于建议的下一次提交中。

无论如何,当您运行时git commit,Git不会使用您的工作树中的内容。那些不是Git 的文件。Git 使用它自己的索引中的任何内容。在此过程中,有几种形式的git commit将文件复制到 Git 的索引中,例如,git commit -a就像git add -u后面跟着git commit- 但是使用额外的索引文件副本的偷偷摸摸的内部实现允许 Git 在出现问题时将其全部备份。但最终,Git 必须将文件放入或至少一个索引中才能提交它们。

使用git worktree

让我们在这里再次注意,你的工作树是的,除了一些 Git 命令,比如git checkout, git restoregit reset --hard例如,告诉 Git用其他地方的文件覆盖我的工作树中的文件。当然,其中之一是一个巨大的例外:git checkout或者git switch最终覆盖文件。

Git 的索引当然是 Git 的。但是,您可以对其进行一些控制:例如,git checkout意味着从某些提交中填充它,git reset意味着删除其中的一部分并从某些提交中将内容放回其中。Agit rm表示从中删除文件,也从您自己的工作树中删除,除非您添加--cached; 并且git add意味着使某些文件的索引副本与工作树副本匹配2

Git 将由 Git 的索引生成的新提交放在当前分支上。如果你想切换到其他当前分支,当然,你必须使用git checkoutor (在 Git 2.21 或更高版本中) git switch,这会覆盖 Git 的索引和你的工作树。但是,如果您可以拥有另一棵工作树而不会打扰您的主要工作树呢?这就是git worktree进来的地方。

使用git worktree add,您可以告诉 Git 创建一个新的、单独的、额外的工作树,与当前存储库一起使用。这个命令有很多限制:例如,新的工作树必须在文件系统中的“其他地方”,并且每个添加的工作树必须在不同的分支名称上,或者使用 Git 的分离 HEAD模式。我不会在这里详细介绍,但这些约束来自 Git 使用的索引和工作树模型:

  • 每个添加的工作树都必须在某个地方,这样它就不会触及这个工作树或任何其他工作树。
  • 每个添加的工作树都带有一个添加的索引,该索引是该特定工作树的私有。
  • 每个添加的工作树都有自己的特殊名称HEAD

这一切的意思是,如果你有一个分支 X 和另一个分支 Y,并且想要同时处理“两个分支”,你可以选择一个进入主存储库——比如说 X——然后使用git worktree add让另一个去:

git checkout X
git worktree add ../branch-Y Y

现在您的主工作树(在 中./)位于分支 X 上,而添加的工作树(位于 中)../branch-Y位于分支 Y 上。您可以在任一分支中进行独立工作。由于每个都有自己的工作树,因此您可以在任一分支中测试事物,而不会影响您在另一个分支中正在进行的工作。但是由于两个工作树都使用相同的底层存储库,因此无论何时您在任一工作树中进行新提交,该新提交都可以在所有其他工作树中查看,或者其他选项(如 rebase)。

这并不像git push . HEAD:otherbranch诀窍那么聪明。但是,在进行此类工作时,保持理智也容易得多。


2您可以说这git add意味着“将文件复制到工作树中”,但如果您首先工作树中删除文件,然后该文件,Git 将删除其索引副本。所以这就是为什么我说它的意思是让它匹配:如果需要,它会删除一个文件,如果这是让它匹配的方法。git add


推荐阅读