章节 ▾ 第二版

7.13 Git 工具 - 替换

替换

我们之前已经强调过,Git 对象数据库中的对象是不可更改的,但是 Git 提供了一种有趣的方法来假装用其他对象替换数据库中的对象。

replace 命令允许你指定 Git 中的一个对象,并说“每次你引用这个对象时,都假装它是不同的对象”。这对于用另一个提交替换历史记录中的一个提交非常有用,而无需使用 git filter-branch 重建整个历史记录。

例如,假设你有一个庞大的代码历史记录,并且想要将你的存储库拆分为一个用于新开发人员的短历史记录,以及一个用于对数据挖掘感兴趣的人的更长更大的历史记录。 你可以通过将新行中的最早提交“替换”为旧行中的最新提交来将一个历史记录嫁接到另一个历史记录上。 这样做很好,因为它意味着你实际上不必像通常将它们连接在一起那样重写新历史记录中的每个提交(因为父系关系会影响 SHA-1)。

让我们来尝试一下。 让我们获取一个现有的存储库,将其拆分为两个存储库,一个最近的和一个历史的,然后我们将看到如何通过 replace 重新组合它们,而无需修改最近的存储库 SHA-1 值。

我们将使用一个具有五个简单提交的简单存储库

$ git log --oneline
ef989d8 Fifth commit
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

我们想将其分解为两条历史记录线。 一条线从提交一到提交四 - 这将是历史的。 第二行将只是提交四和提交五 - 这将是最近的历史。

Example Git history
图 163. Git 历史记录示例

好的,创建历史版本很容易,我们只需在历史记录中创建一个分支,然后将该分支推送到新的远程仓库的 master 分支即可。

$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
Creating a new `history` branch
图 164. 创建新的 history 分支

现在我们可以将新的 history 分支推送到我们新仓库的 master 分支

$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master

好的,我们的历史记录已发布。现在更难的部分是缩减我们最近的历史记录,使其更小。我们需要一个重叠部分,以便我们可以用另一个仓库中的等效提交替换一个提交,所以我们要将此仓库截断为仅包含提交四和五(因此提交四重叠)。

$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

在这种情况下,创建一个包含如何扩展历史记录的说明的基本提交很有用,以便其他开发人员知道如果他们遇到截断历史记录中的第一个提交并且需要更多信息该怎么做。 因此,我们将创建一个初始提交对象作为我们的基点,其中包含说明,然后在它之上重新设置剩余的提交(四和五)的基础。

为此,我们需要选择一个拆分点,对于我们来说,它是第三次提交,在 SHA 术语中是 9c68fdc。 因此,我们的基本提交将基于该树。 我们可以使用 commit-tree 命令创建我们的基本提交,该命令仅接受一棵树,并将为我们提供一个全新的、无父提交对象 SHA-1。

$ echo 'Get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
注意

commit-tree 命令是一组通常被称为“管道”命令的命令之一。 这些命令通常不打算直接使用,而是由其他 Git 命令用来执行较小的作业。 在我们进行像这样更奇怪的事情时,它们允许我们执行真正低级的操作,但并不意味着日常使用。 您可以在 管道与瓷器 中阅读有关管道命令的更多信息。

Creating a base commit using `commit-tree`
图 165. 使用 commit-tree 创建基本提交

好的,现在我们有了一个基本提交,我们可以使用 git rebase --onto 在此基础上重新设置我们历史记录的其余部分的基础。 --onto 参数将是我们刚从 commit-tree 返回的 SHA-1,并且 rebase 点将是第三次提交(我们想要保留的第一个提交的父级,9c68fdc

$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
Rebasing the history on top of the base commit
图 166. 在基本提交之上重新设置历史记录的基础

好的,现在我们已经在一次性使用的基本提交之上重写了我们最近的历史记录,其中现在包含有关如果我们想要如何重构整个历史记录的说明。 我们可以将这个新的历史记录推送到一个新的项目,现在当人们克隆该仓库时,他们将只看到最近的两次提交,然后是一个包含说明的基本提交。

现在让我们切换角色到第一次克隆项目的并且想要整个历史记录的人。 要在克隆这个截断的仓库后获取历史记录数据,必须为历史仓库添加第二个远程仓库并获取

$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master

现在,协作者将在 master 分支中拥有他们最近的提交,并在 project-history/master 分支中拥有历史提交。

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

要合并它们,您可以简单地调用 git replace,其中包含您要替换的提交,然后包含您要替换它的提交。 因此,我们要用 project-history/master 分支中的“第四次”提交替换 master 分支中的“第四次”提交

$ git replace 81a708d c6e1e95

现在,如果您查看 master 分支的历史记录,它看起来像这样

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

很酷,对吧? 无需更改上游的所有 SHA-1,我们就可以用完全不同的提交替换历史记录中的一次提交,并且所有常用的工具(bisectblame 等)都会像我们期望的那样工作。

Combining the commits with `git replace`
图 167. 使用 git replace 合并提交

有趣的是,它仍然显示 81a708d 作为 SHA-1,即使它实际上使用的是我们替换的 c6e1e95 提交数据。 即使您运行像 cat-file 这样的命令,它也会显示您替换的数据

$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit

请记住,81a708d 的实际父项是我们的占位符提交 (622e88e),而不是此处所说的 9c68fdce

另一个有趣的事情是,此数据保存在我们的引用中

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

这意味着我们可以轻松地与他人分享我们的替换,因为我们可以将其推送到我们的服务器,其他人可以轻松地下载它。 这在我们这里介绍的历史嫁接场景中没有那么大的帮助(因为每个人都会下载两个历史记录,那么为什么要分离它们呢?),但在其他情况下可能很有用。

scroll-to-top