这根本不是樱桃采摘。不要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 .在确保它.引用整个索引/工作树对之后运行,确保我们位于工作树的顶部而不是某个子目录/子文件夹中。
现在只有未跟踪的文件保留在我们的工作树中。如果我们愿意,我们也可以删除它们,使用普通的rmor 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 向后读取到HandG等等所产生的历史F,根本没有提到commit K。
相反,您可能想要一个如下所示的历史记录:
...--F--G--H--I <-- master
\ /
J-------K-----L <-- somebranch
在这里, commitI回溯到commits和.H K
进行这种提交变体I有点棘手,因为除了使用管道命令之外,进行提交git commit-tree的唯一方法是使用.Igit 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 内部使用的低级存储格式,否则它们并不明显。“删除所有内容,然后检查不同的提交内容”的方法非常明显,并且很容易记住。