> For the complete documentation index, see [llms.txt](https://b1f6c1c4.gitbook.io/learn-git-the-super-hard-way/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://b1f6c1c4.gitbook.io/learn-git-the-super-hard-way/chapter7.md).

# 第7章：直接操纵commit

## 基础知识

从物理结构上讲，一个commit表示一个完整的版本； 但是，从逻辑结构上讲，一个commit还可以表示 *相比于之前进行了哪些修改* 。

从物理结构上讲，一个ref就是一个commit的指针； 但是，从逻辑结构上讲，一个ref还可以表示 *一系列修改*。

为了简便起见，本章所有commit均为空；当commit非空时，请参照第6章进行merge。

```bash
git init .
# Initialized empty Git repository in /root/.git/
git config alias.lg "log --graph --pretty=tformat:'%h -%d <%an/%cn> %s' --abbrev-commit"
```

## 复制逻辑commit

在开始之前，先创建几个commit：

```bash
git mktree </dev/null
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800

commit A
EOF
# 7b2c8f5d87ac92311b142000cb783ea85d80c3d2
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 7b2c8f5d87ac92311b142000cb783ea85d80c3d2
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800

commit B
EOF
# cd5b86bab3df3d8556c03d03ecbabf136a5ca1da
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent cd5b86bab3df3d8556c03d03ecbabf136a5ca1da
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736002 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736002 +0800

commit C
EOF
# c6e6deb39bb4022a49e0ca72abe328d5bb9db47d
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent c6e6deb39bb4022a49e0ca72abe328d5bb9db47d
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736003 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736003 +0800

commit D
EOF
# 3aec5e9861752b06a109fbacb25e374d28176ea2
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent cd5b86bab3df3d8556c03d03ecbabf136a5ca1da
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736004 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736004 +0800

commit E
EOF
# 2c353e8bf7f03cce07a8b8d8b7c20cbb9ea3d771
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 2c353e8bf7f03cce07a8b8d8b7c20cbb9ea3d771
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736005 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736005 +0800

commit F
EOF
# 85706b3115a9ce4fb25bc1e43e732515d33bb31f
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 2c353e8bf7f03cce07a8b8d8b7c20cbb9ea3d771
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736006 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736006 +0800

commit G
EOF
# 31155de821fe4a68c23714dc16d54d5bd865cb9a
git hash-object -t commit --stdin -w <<EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent c6e6deb39bb4022a49e0ca72abe328d5bb9db47d
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736007 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736007 +0800

commit H
EOF
# f071f8ea1168fa5f7ab26295836e425b6e8d90b9
git update-ref --no-deref refs/heads/br1 f071f8ea
git update-ref --no-deref refs/heads/br2 3aec5e98
git update-ref --no-deref refs/heads/br3 85706b31
git update-ref --no-deref refs/heads/br4 31155de8
git symbolic-ref HEAD refs/heads/br1
git lg br1 br2 br3 br4
# * f071f8e - (HEAD -> br1) <b1f6c1c4/b1f6c1c4> commit H
# | * 31155de - (br4) <b1f6c1c4/b1f6c1c4> commit G
# | | * 85706b3 - (br3) <b1f6c1c4/b1f6c1c4> commit F
# | |/  
# | * 2c353e8 - <b1f6c1c4/b1f6c1c4> commit E
# | | * 3aec5e9 - (br2) <b1f6c1c4/b1f6c1c4> commit D
# | |/  
# |/|   
# * | c6e6deb - <b1f6c1c4/b1f6c1c4> commit C
# |/  
# * cd5b86b - <b1f6c1c4/b1f6c1c4> commit B
# * 7b2c8f5 - <b1f6c1c4/b1f6c1c4> commit A
```

* Lv1

```bash
git reset HEAD -- .
git checkout-index -f -a
git read-tree -m cd5b^ cd5b
git cat-file commit cd5b
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent 7b2c8f5d87ac92311b142000cb783ea85d80c3d2
# author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
# committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
#
# commit B
git hash-object -t commit --stdin -w <<EOF
tree $(git write-tree)
parent $(git rev-parse HEAD)
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736060 +0800

commit B
EOF
# 21ee944892050dd1fe60ca83d529ebcf3cc29265
git reset --soft 21ee9448
```

* Lv3

```bash
git update-ref --no-deref refs/heads/br1 f071f8ea
GIT_COMMITTER_NAME=committer \
GIT_COMMITTER_EMAIL=committer@gmail.com \
GIT_COMMITTER_DATE='1514736060 +0800' \
git cherry-pick --keep-redundant-commits cd5b 2c35
# Already up to date!
# [br1 553810a] commit B
#  Author: b1f6c1c4 <b1f6c1c4@gmail.com>
#  Date: Mon Jan 1 00:00:01 2018 +0800
# Already up to date!
# [br1 d352b36] commit E
#  Author: b1f6c1c4 <b1f6c1c4@gmail.com>
#  Date: Mon Jan 1 00:00:04 2018 +0800
git lg br1 br2 br3 br4
# * d352b36 - (HEAD -> br1) <b1f6c1c4/committer> commit E
# * 553810a - <b1f6c1c4/committer> commit B
# * f071f8e - <b1f6c1c4/b1f6c1c4> commit H
# | * 31155de - (br4) <b1f6c1c4/b1f6c1c4> commit G
# | | * 85706b3 - (br3) <b1f6c1c4/b1f6c1c4> commit F
# | |/  
# | * 2c353e8 - <b1f6c1c4/b1f6c1c4> commit E
# | | * 3aec5e9 - (br2) <b1f6c1c4/b1f6c1c4> commit D
# | |/  
# |/|   
# * | c6e6deb - <b1f6c1c4/b1f6c1c4> commit C
# |/  
# * cd5b86b - <b1f6c1c4/b1f6c1c4> commit B
# * 7b2c8f5 - <b1f6c1c4/b1f6c1c4> commit A
# 注意author信息与cd5b的一致
git cat-file commit HEAD~
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent f071f8ea1168fa5f7ab26295836e425b6e8d90b9
# author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
# committer committer <committer@gmail.com> 1514736060 +0800
#
# commit B
# 注意author信息与2c35的一致
git cat-file commit HEAD
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent 553810a2fbb39fca3fc78ec3376ff64bf995028a
# author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736004 +0800
# committer committer <committer@gmail.com> 1514736060 +0800
#
# commit E
```

## 逻辑撤销commit

逻辑撤销即添加逆向修改。 你完全可以添加不在本分支上的commit的逆。

* Lv1

```bash
(git update-ref --no-deref refs/heads/br1 f071f8ea)
git reset --hard
# HEAD is now at f071f8e commit H
git read-tree -m cd5b cd5b^
git cat-file commit cd5b
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent 7b2c8f5d87ac92311b142000cb783ea85d80c3d2
# author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
# committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
#
# commit B
git hash-object -t commit --stdin -w <<EOF
tree $(git write-tree)
parent $(git rev-parse HEAD)
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736001 +0800

Revert "commit B"

This reverts commit cd5b86bab3df3d8556c03d03ecbabf136a5ca1da.
EOF
# 25911f90caf4b0081d23b3b0ba7e4a66fa61b349
git reset --soft 25911f90caf4b0081d23b3b0ba7e4a66fa61b349
git lg
# * 25911f9 - (HEAD -> br1) <b1f6c1c4/b1f6c1c4> Revert "commit B"
# * f071f8e - <b1f6c1c4/b1f6c1c4> commit H
# * c6e6deb - <b1f6c1c4/b1f6c1c4> commit C
# * cd5b86b - <b1f6c1c4/b1f6c1c4> commit B
# * 7b2c8f5 - <b1f6c1c4/b1f6c1c4> commit A
(git reset --soft HEAD~)
```

* Lv3

`git revert`：对若干commits依次执行以下操作：

* `git read-tree -m <commit> <commit>^` （注意：此处方向反过来了）
* `git commit-tree $(git write-tree) -p HEAD`
  * author信息沿用原author的
* `git reset --soft <new-commit>`

遗憾的是，`git revert`并不支持`--allow-empty`，这里就不演示了。

## 详解`git rebase`

### 基本语义

为了更方便地执行大规模commit复制，`git rebase`应运而生。 基本用法是：

* `git rebase [--no-ff] --keep-empty [-i] [--onto <newbase>] [<upstream> [<branch>]]`
  * 若省略`--onto`，则`<newbase>=<upstream>`
  * 若省略`<branch>`，则为`git symbolic-ref HEAD`
  * 若省略`<upsteram>`，则为`<branch>`的默认remote
* `git rebase [--no-ff] --keep-empty [-i] --onto <newbase> --root [<branch>]`
  * 若省略`<branch>`，则为`git symbolic-ref HEAD`
* `git rebase [--no-ff] --keep-empty [-i] --root [<branch>]`
  * 若省略`<branch>`，则为`git symbolic-ref HEAD`

第一种rebase是把upsteram到branch的所有逻辑修改（选择性地）应用到newbase上。 依次执行以下命令：

* `git rev-list ^<upstream> <branch> > git-rebase-todo`
* `vim git-rebase-todo`
* `git update-ref <branch> <newbase>`
* `git symbolic-ref HEAD <branch>`
* `git reset --hard`
* 根据todo文件的内容，对每一个非drop项执行：
  * `git read-tree -m <commit>^ <commit>`
  * `git checkout-index -fua`
  * pick项：
    * 如果没有`--no-ff`且`<commit>`以HEAD为parent，则`git reset --hard <commit>`
    * 不然，`git commit-tree $(git write-tree) -p HEAD`
  * squash和fixup项：
    * `git commit-tree $(git write-tree) -p HEAD~`
  * `git reset --soft <new-commit>`

第二种rebase是把branch的所有逻辑修改（选择性地）应用到newbase上。 依次执行以下命令：

* `git rev-list ^<newbase> <branch> > git-rebase-todo`
* `vim git-rebase-todo`
* `git update-ref <branch> <newbase>`
* `git symbolic-ref HEAD <branch>`
* `git reset --hard`
* 根据todo文件的内容，对每一个非drop项执行：
  * `git read-tree -m <commit>^ <commit>`
  * `git checkout-index -fua`
  * pick项：
    * 如果没有`--no-ff`且`<commit>`以HEAD为parent，则`git reset --hard <commit>`
    * 不然，`git commit-tree $(git write-tree) -p HEAD`
  * squash和fixup项：
    * `git commit-tree $(git write-tree) -p HEAD~`
  * `git reset --soft <new-commit>`

第三种rebase是把branch的所有逻辑修改（选择性地）重新实现一遍。 依次执行以下命令：

* `git rev-list <branch> > git-rebase-todo`
* `vim git-rebase-todo`
* `git symbolic-ref HEAD <branch>`
* `git rm -rf .`
* 根据todo文件的内容，对第一个非drop项执行：
  * `git read-tree -m <commit>^ <commit>`
  * `git checkout-index -fua`
  * pick项：
    * 如果没有`--no-ff`且`<commit>`没有parent，则`git reset --hard <commit>`
    * 不然，`git commit-tree $(git write-tree)`
  * `git reset --soft <new-commit>`
* 根据todo文件的内容，对其余每一个非drop项执行：
  * `git read-tree -m <commit>^ <commit>`
  * pick项：
    * 如果没有`--no-ff`且`<commit>`以HEAD为parent，则`git reset --hard <commit>`
    * 不然，`git commit-tree $(git write-tree) -p HEAD`
  * squash和fixup项：
    * `git commit-tree $(git write-tree) -p HEAD~`
  * `git reset --soft <new-commit>`

### 示例

```bash
git update-ref --no-deref refs/heads/br1 f071f8ea
git update-ref --no-deref refs/heads/br2 3aec5e98
git update-ref --no-deref refs/heads/br3 85706b31
git update-ref --no-deref refs/heads/br4 31155de8
git symbolic-ref HEAD refs/heads/br1
git lg br1 br2 br3 br4
# * f071f8e - (HEAD -> br1) <b1f6c1c4/b1f6c1c4> commit H
# | * 31155de - (br4) <b1f6c1c4/b1f6c1c4> commit G
# | | * 85706b3 - (br3) <b1f6c1c4/b1f6c1c4> commit F
# | |/  
# | * 2c353e8 - <b1f6c1c4/b1f6c1c4> commit E
# | | * 3aec5e9 - (br2) <b1f6c1c4/b1f6c1c4> commit D
# | |/  
# |/|   
# * | c6e6deb - <b1f6c1c4/b1f6c1c4> commit C
# |/  
# * cd5b86b - <b1f6c1c4/b1f6c1c4> commit B
# * 7b2c8f5 - <b1f6c1c4/b1f6c1c4> commit A
```

第一种rebase：

```bash
(git update-ref --no-deref refs/heads/br1 f071f8ea)
(git update-ref --no-deref refs/heads/br2 3aec5e98)
(git update-ref --no-deref refs/heads/br3 85706b31)
(git update-ref --no-deref refs/heads/br4 31155de8)
(git symbolic-ref HEAD refs/heads/br1)
GIT_COMMITTER_NAME=committer \
GIT_COMMITTER_EMAIL=committer@gmail.com \
GIT_COMMITTER_DATE='1514736120 +0800' \
git rebase --quiet --no-ff --keep-empty --onto br2 br3 br4
git lg br1 br2 br3 br4
# * 4da7f18 - (HEAD -> br4) <b1f6c1c4/committer> commit G
# * 3aec5e9 - (br2) <b1f6c1c4/b1f6c1c4> commit D
# | * f071f8e - (br1) <b1f6c1c4/b1f6c1c4> commit H
# |/  
# * c6e6deb - <b1f6c1c4/b1f6c1c4> commit C
# | * 85706b3 - (br3) <b1f6c1c4/b1f6c1c4> commit F
# | * 2c353e8 - <b1f6c1c4/b1f6c1c4> commit E
# |/  
# * cd5b86b - <b1f6c1c4/b1f6c1c4> commit B
# * 7b2c8f5 - <b1f6c1c4/b1f6c1c4> commit A
```

第二种rebase：

```bash
(git update-ref --no-deref refs/heads/br1 f071f8ea)
(git update-ref --no-deref refs/heads/br2 3aec5e98)
(git update-ref --no-deref refs/heads/br3 85706b31)
(git update-ref --no-deref refs/heads/br4 31155de8)
(git symbolic-ref HEAD refs/heads/br1)
GIT_COMMITTER_NAME=committer \
GIT_COMMITTER_EMAIL=committer@gmail.com \
GIT_COMMITTER_DATE='1514736120 +0800' \
git rebase --quiet --no-ff --keep-empty --onto br2 --root br4
git lg br1 br2 br3 br4
# * 979feed - (HEAD -> br4) <b1f6c1c4/committer> commit G
# * b70ca17 - <b1f6c1c4/committer> commit E
# * 3aec5e9 - (br2) <b1f6c1c4/b1f6c1c4> commit D
# | * f071f8e - (br1) <b1f6c1c4/b1f6c1c4> commit H
# |/  
# * c6e6deb - <b1f6c1c4/b1f6c1c4> commit C
# | * 85706b3 - (br3) <b1f6c1c4/b1f6c1c4> commit F
# | * 2c353e8 - <b1f6c1c4/b1f6c1c4> commit E
# |/  
# * cd5b86b - <b1f6c1c4/b1f6c1c4> commit B
# * 7b2c8f5 - <b1f6c1c4/b1f6c1c4> commit A
```

第三种rebase：

```bash
(git update-ref --no-deref refs/heads/br1 f071f8ea)
(git update-ref --no-deref refs/heads/br2 3aec5e98)
(git update-ref --no-deref refs/heads/br3 85706b31)
(git update-ref --no-deref refs/heads/br4 31155de8)
(git symbolic-ref HEAD refs/heads/br1)
GIT_AUTHOR_NAME=author \
GIT_AUTHOR_EMAIL=author@gmail.com \
GIT_AUTHOR_DATE='1234567890 +0800' \
GIT_COMMITTER_NAME=committer \
GIT_COMMITTER_EMAIL=committer@gmail.com \
GIT_COMMITTER_DATE='1514736120 +0800' \
git rebase --quiet --no-ff --keep-empty --root br4
git lg br1 br2 br3 br4
# * 67df3f7 - (HEAD -> br4) <b1f6c1c4/committer> commit G
# * 23eb8c6 - <b1f6c1c4/committer> commit E
# * 5aaf590 - <b1f6c1c4/committer> commit B
# * 71fcb5a - <b1f6c1c4/committer> commit A
# * f071f8e - (br1) <b1f6c1c4/b1f6c1c4> commit H
# | * 85706b3 - (br3) <b1f6c1c4/b1f6c1c4> commit F
# | * 2c353e8 - <b1f6c1c4/b1f6c1c4> commit E
# | | * 3aec5e9 - (br2) <b1f6c1c4/b1f6c1c4> commit D
# | |/  
# |/|   
# * | c6e6deb - <b1f6c1c4/b1f6c1c4> commit C
# |/  
# * cd5b86b - <b1f6c1c4/b1f6c1c4> commit B
# * 7b2c8f5 - <b1f6c1c4/b1f6c1c4> commit A
```

## 对merge的处理

`git rebase`有四种处理merge的模式。 其中`--preserve-merge`由于bug连篇建议在任何情况下都不要使用。

下面分别对三种rebase讨论其他三种处理方式的语义。

* 第一种rebase：对于`^<upstream> <branch>`
  * 默认：
    * 先对所有待rebase的DFS排序
    * 再去掉所有包含多于1个parent的commit
    * 依次应用到`<newbase>`上
  * `--rebase-merges[=-no-rebase-cousins]`：
    * 原没有parent，现依然没有parent
    * 原以`<upstream>^!`为直接parent，现以`<newbase>`为直接parent
    * 原以`<upstream>^@`为直接parent，parent不变
    * 原以`^<upstream> <branch>`为直接parent，现以新创建的相应commit为parent
  * `--rebase-merges=rebase-cousins`：
    * 原没有parent，现以`<newbase>`为parent
    * 原以`<upstream>^!`为直接parent，变成以`<newbase>`为直接parent
    * 原以`<upstream>^@`为直接parent，parent不变
    * 原以`^<upstream> <branch>`为直接parent，现以新创建的相应commit为parent
* 第二种rebase：对于`^<newbase> <branch>`
  * 默认：
    * 先对所有待rebase的DFS排序
    * 再去掉所有包含多于1个parent的commit
    * 依次应用到`<newbase>`上
  * `--rebase-merges[=-no-rebase-cousins]`：
    * 原没有parent，现依然没有parent
    * 原以`<newbase>`为直接parent，parent不变
    * 原以`^<newbase> <branch>`为直接parent，现以新创建的相应commit为parent
  * `--rebase-merges=rebase-cousins`：
    * 原没有parent，现以`<newbase>`为parent
    * 原以`<newbase>`为直接parent，parent不变
    * 原以`^<newbase> <branch>`为直接parent，现以新创建的相应commit为parent
* 第三种rebase：对于`<branch>`
  * 默认：
    * 先对所有待rebase的DFS排序
    * 再去掉所有包含多于1个parent的commit
    * 从零开始复制
  * `--rebase-merges[=-no-rebase-cousins]`：同下
  * `--rebase-merges=rebase-cousins`：
    * 原没有parent，现依然没有parent
    * 原以`<branch>`为直接parent，现以新创建的相应commit为parent

## 总结

* Lv2
  * `git cherry-pick --keep-redundant-commits <commit-ish>...`
* Lv3
  * `git revert <commit-ish>...`
  * `git rebase --keep-empty [-i] [--onto <newbase>] [(<upstream>|--root) [<branch>]]`
    * `[--no-ff]`
    * `[--rebase-merges[=[no-]rebase-cousins]]`
