要将提交从一个分支移动到另一个分支,我需要在目标分支上
问题是,当我在原始分支时,是否可以复制提交
TL;DR
考虑git worktree
。跳到最后看看如何操作,但从中间读一读,看看为什么。
Long
要将提交从一个分支移动到另一个分支,我需要在目标分支上。
事实并非如此。这种说法源于对Git提交和分支名称工作方式的误解。但这是一个可以理解的错误,因为Git提交执行的方式非常令人困惑,最终用户的目标通常不是移动提交,而是将提交的效果复制到新的提交中。
这些术语都有点笨拙:
问题是,当我在原始分支中时,是否可以复制提交。
是的,但这里有一个重大问题,具体取决于复制提交的意思。您的标记提到了git cherry-pick
,它将提交转换为一组更改,然后将同一组更改应用于其他提交。这是";复制";一个承诺,很可能就是你的意思,也是当";在原始分支中";(这个短语也有点笨拙:你从来没有在分支中真正,尽管你可以在分支上)。
以下是发生的事情:
-
在Git中,提交是真实存在的,具有永久性——嗯,大多是永久性——你可以从一个Git存储库转移到另一个存储库。每个提交都有编号,带有一个唯一但看起来随机的哈希ID。每个提交都保存每个文件的完整快照,以及一些元数据:例如,关于谁提交、何时提交以及为什么提交的信息(他们的日志消息)。
-
每个提交都在其元数据中存储一些早期提交的哈希ID。大多数提交都只存储一个这样的原始哈希ID,我们称之为提交的父级。
-
分支名称在Git中是临时的和短暂的。他们来来去去随你。从某种意义上说,它们不是真正的:它们的行为很像黄色的小便签。
你只需在一次提交中粘贴任意多的便签,就可以写一个不同的"分支机构名称";在每个音符上。该提交现在是所有分支上的最后一个提交。剥去提交的便笺,将其粘贴在一些其他的提交上,现在您新选择的(现有的)提交是该分支上的最后一次提交。
你选择这些便签中的一个作为你的";当前分支";将不同颜色的便签粘贴到黄色便签上,上面有分支机构的名称。(例如,让我们用绿色表示HEAD
。)
每当您使用git commit
进行新的提交时,或者通过结束樱桃选择或合并或其他方式,Git都会将该新提交放置在当前分支的顶端。它通过:
- 查找哪个分支名称上粘贴了
HEAD
(附加到它) - 将newcommit的new哈希ID(此新commit唯一)填充到适当的分支名称中,或者,在我们的类比中,从旧的分支顶端剥离适当的黄色便笺,并将其粘贴到新commit上
新提交的父级提交是旧的分支提示提交。也就是说,假设我们从开始
... <-F <-G <-H <-- somebranch (HEAD)
名称CCD_ 6——我们的";黄色便签";上面写有somebranch
——表示此提交链的最后提交是提交H
。
提交H
在其元数据中有早期提交G
的原始哈希ID。因此Git可以使用提交H
的内容来定位提交G
。
提交G
当然也是一个提交,所以它有元数据,包括一些更早提交F
的原始哈希ID。因此,Git可以使用G
来查找F
。找到F
后,Git将能够在历史上再跳一步,依此类推,从现在开始,在所有历史中向下跳。
Git无法以这种方式向前移动。Git只能后退。要找到提交H
——链中最后一个提交——Git需要保存H
的哈希ID。那个东西是我们的分店名称。
但现在我们做出了新的承诺。这个新提交需要一个父级:Git将其父级设置为当前提交的哈希IDH
。因此,新提交I
指向现有提交H
:
... <-F <-G <-H <-I
完成后,Git现在将I
的哈希ID写入名称somebranch
,更新黄色便签:
... <-F <-G <-H <-I <-- somebranch (HEAD)
HEAD
的绿色便签仍然附着在同一张旧的黄色便签上;只是标记为somebranch
的黄色便笺上的数字现在是提交I
的实际原始哈希ID。
这就是为什么我们可以将提交从一个分支移动到另一个分支
假设我们有以下提交:
I--J <-- dev
/
...--F--G--H <-- master
如果我们只是让Git向前滑动名称master
——这是Git自己无法做到的;我们必须给它命名为dev
,这样它才能找到提交J
——我们得到:
...--F--G--H--I--J <-- dev, master
委员会I
和J
现在都在两个分支上,尽管以前它们只在dev
分支上。现在,如果我们愿意的话,我们可以完全删除dev
名称:我们有它是为了找到提交J
,而名称master
找到了提交H
。如果删除dev
,则所有提交现在都只在master
上。
事实上,提交本身根本没有移动。移动的是分支名称,然后我们删除了一个。分支名称其实并不重要,只有一个重要的例外:它们让我们找到最后一个提交。如果我们根本没有名字,我们就根本找不到提交。还有其他类型的名称——例如标签名称和远程跟踪名称等等——但我们需要一些名称来表示";最后一个";以任何顺序提交,这样我们就可以很容易地命名该提交。
然后,从那里,我们可以让git log
或其他一些Git操作反向工作。这发现了一些早期的承诺。如果之前的承诺看起来特别重要(或令人心酸或兴奋或其他什么),我们可以直接在它上面加上一个名字,这样就很容易找到它。原始哈希ID是Git真正找到它的方式,但名称给了我们人类我们可以处理的东西:看起来随机的哈希ID太难了。
樱桃采摘
正如我前面提到的,git cherry-pick
所做的是";复制";承诺。现在,提交包含一个完整的快照,而不是一组更改。但是提交也包含一个父散列ID。父提交也有一个快照。假设我们有一些提交链:
...--G--H <-- main
I--J--K <-- branch
我们刚刚修复了提交K
中的一些可怕错误,并希望立即将相同的更改直接导入main
。嗯,K
有一个快照,但它的快照是从J
的快照构建的,并对其进行了一些更改
但是Git可以很容易地向我们展示K
中的与J
中的有什么不同。对于哈希ID为K
的提交,git showhash
或git log -p
会向我们显示这一点。(事实上,我们可以运行git show branch
,因为Git会将名称branch
转换为我们的哈希ID。)Git只需将两个快照(一个用于J
,另一个用于K
)提取到内存中的临时区域中,然后将两者进行比较。对于每个相同的文件,它什么也没说,对于每个不同的文件,我们可以看到发生了什么变化。
我们可以接受这组更改——从J
到K
的差异——并让Git将这组更改应用于任何其他提交。如果我们首先使用git checkout
或git switch
将HEAD
连接到somebranch
0,那么我们就有了这个:
...--G--H <-- main (HEAD)
I--J--K <-- branch
那么当前提交,我们已经将其快照Git提取到工作树中,是来自提交H
的快照。我们现在可以运行:
git cherry-pick branch
让Git比较J
和K
——Git从名称branch
和提交K
中的元数据中找到两个哈希ID——并将这些相同的更改应用于我们当前的提交H
。
从技术上讲,这个应用程序实际上使用了Git的merge代码。这种合并可能会有合并冲突,如果发生冲突,Git将需要同时写入Git自己的暂存区和我们的工作树。在这种情况下,Git将在之后因合并冲突错误而停止,清理混乱并完成流程将成为我们的工作。但如果一切顺利,Git将能够自己进行合并,然后像往常一样进行普通的提交。这个普通的提交将以H
作为其父级,并将获得一个新的、唯一的哈希ID。我们可以称之为L
,但由于其效果将与K
的效果相同,并且其commit消息来自K
的元数据中的提交消息,我倾向于将其称为K'
:
...--G--H--K' <-- main (HEAD)
I--J--K <-- branch
请注意,所有这些都要求我们在启动时将main
签出为当前分支,并将提交H
签出为当前提交。完成后,提交K'
是当前提交,名称main
选择提交K'
。
使用git worktree
问题是,当我在原始分支中时,是否可以复制提交。
假设您完全掌握了我刚才绘制的起始情况,但您使用的是branch
,并且在制作了K
之后,您已经开始研究将如何很快提交L
:
...--G--H <-- main
I--J--K <-- branch (HEAD)
要切换到main
,您需要将当前工作提交到某个地方——可能使用git stash
(进行提交),也可能只是提前一点使用git commit
(我在git worktree
存在之前使用的方法)——然后您可以使用git switch main
进入main
并开始挑选。
不过,为了避免到目前为止破坏您的工作,您可以使用git worktree add
创建一个新的工作树来避免所有这些。这个新的工作树带有自己的新HEAD
和索引/暂存区。然后,新的工作树将从某个现有或新的分支名称填充,就像由git checkout
或git switch
填充一样(作为git worktree
选项的一部分,您可以选择是创建新分支名称,还是使用某个现有分支名称)。
因此,假设您处于现有工作树的顶层,您可能会运行:
git worktree add ../project.main main
它将创建../project.main
,输入新的空目录,创建一个将新目录连接到现有存储库的.git
文件,然后从main
分支填充新的工作树。这个新的工作树是main
分支上的。然后,您可以创建一个新窗口,或者只使用现有窗口:
pushd ../project.main
例如(假设bash或类似情况)。此添加的工作树中的git status
将显示您在分支main
上,而默认工作树的git status
将显示您位于分支branch
上。
您在添加的工作树中所做的提交将进入(共享)存储库,并更新您在添加工作树中的分支名称。当你完成添加的工作树时,你可以简单地将其完全删除:
popd
rm -rf ../project.main
然后运行git worktree prune
,让Git知道添加的工作树不见了:
git worktree prune
(当前的Git版本有git worktree remove
,可以在一步中删除和修剪添加的工作树,但早期的Git版缺乏这一额外功能,需要两步删除和修剪。请注意,虽然git worktree
在Git 2.5中是新的,但它有一个直到Git 2.15才修复的严重错误:如果您使用的是Git版本>=2.5但<2.15,并使用git worktree add
,请在两周后,你就可以避免被这种虫子咬了。)