章节 ▾ 第二版

7.13 Git 工具 - 替换

替换

正如我们之前强调的,Git 对象数据库中的对象是不可更改的,但 Git 提供了一种有趣的方式,可以假装将其数据库中的对象替换为其他对象。

replace 命令允许你指定 Git 中的一个对象,并告诉 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)”命令的命令集之一。这些命令通常不直接使用,而是由其他 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. 在基础提交之上变基历史记录

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

现在让我们转换角色,假设有人第一次克隆项目并想要完整的历史记录。在克隆了这个截断的仓库之后,要获取历史数据,就需要为历史仓库添加第二个远程仓库并抓取(fetch)数据。

$ 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