git - 如何从子分支直接提交到父分支?
问题描述
假设我在基于分支 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 至少为 2.15 或更高版本,我建议git worktree add
您出于理智的目的进行调查。
背景
让我们画出这个,并确保我们都同意这一切的含义。我们从提交有父节点的事实开始,所以如果我们绘制一个简单的线性提交链,使用大写字母代表实际的哈希 ID,我们会得到这样一张图片:
... <-F <-G <-H
whereH
代表某个提交的哈希 ID,它是某个分支上的最后一次提交。提交H
由两部分组成:所有文件的源快照和一些元数据。在提交的元数据中H
,Git 存储了先前提交的实际哈希 ID G
,因此H
指向G
. 提交G
是类似的:它存储一个快照和一些元数据,并且元数据导致G
向后指向F
,它本身向后指向。这个向后看的提交链是存储库中的历史记录。
分支名称出现在这张图片中的一点是,它们让我们(和 Git)通过存储每个“最后一次提交”的哈希 ID 来找到提交。因此,如果我们有分支br1
和br2
- 两个不同的分支名称 -都具有H
,或者更确切地说是 的完整大丑哈希 ID H
,在它们中,我们有这样的图片:
...--F--G--H <-- br1, br2
每当我们进行新提交时,Git 都会将新提交写出——它会写入快照和一些元数据——并将新提交的父哈希设置为当前提交。写出新的提交会为其分配新的、大而丑陋的随机哈希 ID,我们将其称为I
,因此它I
指向H
:
...--G--H
\
I
然后最后一步git commit
是将I
'hash ID'写入当前分支名。
为了完成所有这些工作,Git 需要知道两件事:
- 当前分支的名称是什么?
- 当前提交的哈希 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-A
和branch-B
:
...--G--H <-- branch-A
\
I--J--K <-- branch-B (HEAD)
我对在分支 B 上完成的提交感到满意,我想将该提交“推送”到分支 A 并以此为基础。
您希望包含I-J-K
当前仅在的三个提交中的哪一个?在不进行任何新提交的情况下,此时您唯一的选择是将标签移动到指向、或。如果你选择指向它,你会得到:branch-B
branch-A
branch-A
I
J
K
I
...--G--H--I <-- branch-A
\
J--K <-- branch-B (HEAD)
这实现了你想要的,但如果你选择指向它,J
你会得到:
...--G--H--I--J <-- branch-A
\
K <-- branch-B (HEAD)
这有效地将提交和I
移动 J
到branch-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 迁移到H
,I
因为I
hasH
作为父级。也可以从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 --cached
or之类的拼写中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 restore
,git reset --hard
例如,告诉 Git用其他地方的文件覆盖我的工作树中的文件。当然,其中之一是一个巨大的例外:git checkout
或者git switch
最终覆盖文件。
Git 的索引当然是 Git 的。但是,您可以对其进行一些控制:例如,git checkout
意味着从某些提交中填充它,并git reset
意味着删除其中的一部分并从某些提交中将内容放回其中。Agit rm
表示从中删除文件,也从您自己的工作树中删除,除非您添加--cached
; 并且git add
意味着使某些文件的索引副本与工作树副本匹配。2
Git 将由 Git 的索引生成的新提交放在当前分支上。如果你想切换到其他当前分支,当然,你必须使用git checkout
or (在 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
推荐阅读
- ios - 更新 Xcode 11.1 的约束问题
- python - 将 curl 请求转换为 python 请求
- laravel - 如何从多个关系中获取数据
- javascript - 验证月/年是否大于当前日期
- flutter - 在 Flutter 中将 Widget 从单独文件添加到主文件
- javascript - 在 JavaScript 中,属性名称周围的花括号是什么?
- osisoft - 如何在 PI-Web API 中通过属性名称和属性值查找元素
- laravel - Laravel - 如何在不活动 30 分钟后自动注销并重定向到登录页面
- python - 如何在情节中为ticktext着色?
- python - 过滤查询集时forms.py中的Django MultiValueKeyDictError