这根本不是樱桃采摘。不要git cherry-pick
用来制造它:git commit
用来制造它。这是一个非常简单的食谱1:
$ git checkout <branch> # get on the target branch
$ cd $(git rev-parse --show-toplevel) # ensure you're at the top of the work-tree
$ git rm -r . # remove all tracked files from index and work-tree
$ git checkout <hash> -- . # create every file anew from <hash>
$ git commit # make a new commit with all new info
如果您想从 commit 复制提交消息等<hash>
,请考虑添加-c <hash>
到该git commit
行。
1这不是最简单的,但应该可以理解。更简单的在初始之后使用管道命令git checkout
,例如:
git read-tree -u <hash>
git commit
或者:
git merge --ff-only $(git commit-tree -p HEAD <hash>^{tree} < /tmp/commit-msg)
(未经测试,对于第二个,您必须构建一个提交消息)。
长
请记住,Git 存储提交,每个提交都是所有源文件的完整快照,以及一些元数据。每个提交的元数据包括提交者的姓名和电子邮件地址;提交时的日期和时间戳;一条日志消息,说明提交的原因;并且,对于 Git 来说至关重要的是提交父级的哈希 ID。
每当您拥有某个提交的哈希 ID 时,我们就说您指向该提交。如果一个提交具有另一个提交的哈希 ID,则具有哈希 ID 的提交指向另一个提交。
这意味着嵌入在每个提交中的这些哈希 ID 形成了一个向后看的链。如果我们使用单个字母来代表提交,或者按顺序编号它们,我们得到C1
:C2
A <-B <-C <-D ... <-G <-H
或者:
C1 <-C2 <-C3 ... <-C7 <-C8
每个提交的实际名称当然是一些丑陋的哈希 ID,但是使用这样的字母或数字可以让我们作为人类更容易处理它们。在任何情况下,关键是如果我们以某种方式保存链中最后一次提交的哈希 ID,我们最终能够向后跟踪链的其余部分,一次一个提交。
我们让 Git 存储这些哈希 ID 的地方是分支名称。所以像这样的分支名称master
只存储了 commit 的真实哈希 ID H
,而H
它本身存储了它的 parent 的哈希 ID G
,它存储了它的 parent 的哈希 ID F
,依此类推:
... <-F <-G <-H <-- master
这些向后看的链接,从H
到G
到F
,加上每次提交保存的快照以及关于谁提交和为什么提交的元数据,都是你的存储库中的历史。要保留以 结尾的历史记录H
,您只需要确保下一次提交,当您提交时,H
它的父项是:
...--F--G--H--I <-- master
通过进行新提交,Git 更改名称master
以记住新提交的哈希 ID I
,其父级为H
,其父级为 (still) G
,依此类推。
您的目标是I
使用与其他提交相关联的快照K
进行提交,如下所示:
...--F--G--H <-- master
\
J------K------L <-- somebranch
Git 实际上是根据index中的内容而不是源代码树中的内容构建新提交。因此,我们首先git checkout master
将 commitH
设置为当前提交和master
当前分支,它从 commit 的内容中填充索引和工作树H
。
接下来,我们希望索引与提交匹配K
——除了其中的文件之外没有其他文件——所以K
我们首先从索引中删除每个文件。为了理智(即,这样我们可以看到我们在做什么),我们让 Get 对工作树做同样的事情,它会自动完成。因此,我们git rm -r .
在确保它.
引用整个索引/工作树对之后运行,确保我们位于工作树的顶部而不是某个子目录/子文件夹中。
现在只有未跟踪的文件保留在我们的工作树中。如果我们愿意,我们也可以删除它们,使用普通的rm
or git clean
,尽管在大多数情况下它们是无害的。如果您想删除它们,请随意这样做。然后我们需要填写索引——工作树再次出现——从 commit 开始K
,所以我们运行git checkout <hash-of-K> -- .
。这-- .
很重要:它告诉 Git不要切换提交,只需从此处命名的提交中提取所有内容。 我们的索引和工作树现在匹配 commit K
。
(如果提交K
包含我们在 中的所有文件H
,我们可以跳过该git rm
步骤。我们只需要git rm
删除在但H
不在的文件K
。)
最后,现在我们有了与 commit 匹配的索引(和工作树)K
,我们可以安全地进行新的提交,它类似于K
但不连接到 K
.
如果要合并,请使用git merge --no-commit
上述序列导致:
...--F--G--H--I <-- master
\
J-------K-----L <-- somebranch
commit中保存的源快照与 commit 中的I
完全匹配K
。但是,通过阅读master
,发现它指向I
,然后将 commitI
和 on 向后读取到H
andG
等等所产生的历史F
,根本没有提到commit K
。
相反,您可能想要一个如下所示的历史记录:
...--F--G--H--I <-- master
\ /
J-------K-----L <-- somebranch
在这里, commitI
回溯到commits和.H
K
进行这种提交变体I
有点棘手,因为除了使用管道命令之外,进行提交git commit-tree
的唯一方法是使用.I
git merge
在这里,简单的方法是运行git merge -s ours --no-commit
,如下所示:
$ git merge -s ours --no-commit <hash> # start the merge but don't finish it
$ git rm -r . # discard everything
$ git checkout <hash> -- . # replace with their content
$ git commit # and now finish the merge
我们用-s ours
这里来让事情进展得更快更顺利。我们正在构建的实际上是 的结果git merge -s theirs
,除了没有git merge -s theirs
. -s ours
手段忽略他们的提交,只保留我们提交的内容H
。 然后我们把它扔掉并用他们提交的内容替换它K
,然后我们完成合并以获得一个I
指向H
和的合并提交K
。
和以前一样,有一些管道命令技巧可以让这变得更加容易。除非您了解 Git 内部使用的低级存储格式,否则它们并不明显。“删除所有内容,然后检查不同的提交内容”的方法非常明显,并且很容易记住。