章节 ▾ 第二版

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 命令是一组通常被称为“底层命令(plumbing commands)”的命令之一。这些命令通常不直接使用,而是由其他 Git 命令用于完成较小的任务。在我们做一些奇怪的事情时,它们允许我们做一些非常低级别的事情,但它们不适用于日常使用。你可以在底层命令和上层命令中阅读更多关于底层命令的信息。

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

好的,现在我们有了基础提交,我们可以使用 git rebase --onto 将剩余的历史记录在其之上进行变基。--onto 参数将是我们刚刚从 commit-tree 得到的 SHA-1,变基点将是第三个提交(我们想要保留的第一个提交的父级,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

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