Git
章节 ▾ 第二版

7.13 Git 工具 - 替换

替换

正如我们之前强调的那样,Git 的对象数据库中的对象是不可变的,但 Git 提供了一个有趣的方法来 *模拟* 用其他对象替换数据库中的对象。

replace 命令允许您指定 Git 中的一个对象,并说 "每当您引用 *这个* 对象时,就假装它是 *另一个* 对象"。这在用另一个提交替换历史记录中的一个提交时最有用,而无需使用诸如 git filter-branch 之类的命令重建整个历史记录。

例如,假设您有一个庞大的代码历史记录,并且希望将您的仓库拆分为两个历史记录:一个简短的历史记录供新开发者使用,另一个更长、更大的历史记录供对数据挖掘感兴趣的人使用。您可以通过将新行中的第一个提交 "替换" 为旧行中的最后一个提交来将一个历史记录嫁接到另一个历史记录上。这样做的好处是,您实际上不必重写新历史记录中的每个提交,就像通常为了将它们组合在一起而必须做的那样(因为父提交会影响 SHA-1)。

让我们尝试一下。我们拿一个现有的仓库,将其拆分为两个仓库:一个最近的仓库和一个历史仓库,然后我们将看看如何在不修改最近仓库的 SHA-1 值的情况下使用 replace 将它们重新组合。

我们将使用一个包含五个简单提交的简单仓库。

$ 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 [email protected]: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 命令集中的一员,通常被称为“管道”命令。这些命令通常不直接使用,而是由**其他** 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 <[email protected]> 1268712581 -0700
committer Scott Chacon <[email protected]> 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