git cherry pick, reorder commit & delete commit

出于一些原因,我们期望更改 git 的历史记录,比如说

  1. 上传某个特定的提交
  2. 修改提交之间的顺序
  3. 删除某个特定的提交

现在我们来看一下这些操作是如何实现的。

为了便于讲解,我们假定当前本地 master 分支有如下四条历史,而远程 master 分支只有 base commit 一条记录。

1
2
3
4
8b4eefb contrib 3
a3ca1ef contrib 2
ec94292 contrib 1
2d0ae8e base commit


上传某个特定的提交

假设 contrib 2 修复了一个 bug,需要立即上传至远程服务器。它处于 contrib 1 和 contrib 3 之间,如何能只上传 contrib 2 这个提交呢?

回想一下,git push 的语法是

1
git push remote_address local_branch:remote_branch

如果我们可以在本地创建一个分支,使它的历史为 base commit => contrib 2,再上传到服务器就好了。

1
2
3
4
5
6
7
8
9
10
11
# 回到 base commit
git checkout 2d0ae8e

# 创建临时分支
git checkout -b temp

# 获取 contrib 2,会自动创建一个新提交
git cherry-pick a3ca1ef

# 上传代码
git push ssh://user_name@git_server:port_number/repo_name temp:master


修改提交之间的顺序

上面的问题有一个更加优雅的解法。如果我们交换 contrib 1 和 contrib 2 的顺序,就可以避免以后 git pull --rebase 获取新记录时还要修复本地 master 分支。修改它们之间的顺序可以按如下步骤操作:

1
2
3
4
5
# 回到 master
git checkout master

# 交互式 rebase 至 base commit
git rebase -i 2d0ae8e

此时 git 会在编辑器中从上到下显示提交的顺序:

1
2
3
pick ec94292 contrib 1
pick a3ca1ef contrib 2
pick 8b4eefb contrib 3

交换前两行,让 git 先处理 contrib 2 再处理 contrib 1:

1
2
3
pick a3ca1ef contrib 2
pick ec94292 contrib 1
pick 8b4eefb contrib 3

保存后我们就得到了期望的历史记录:

1
2
3
4
7611175 contrib 3
4c99eba contrib 1
2302bf1 contrib 2
2d0ae8e base commit


删除某个特定的提交

如上一节所述,若选用 cherry pick 方法,将 contrib 2 上传到远程 master 分支,那么你还需要修复本地 master 分支。具体来说,在上传 contrib 1 和 contrib 3 前,需要先删除 contrib 2 的记录,然后将 contrib 1 和 contrib 3 一起 rebase 到从服务器获取的 contrib 2 之上。在本地删除一个特定提交又有 cherry pick 和 rebase 两种做法。

cherry pick 需要创建一个临时分支,收集应当保留的提交,然后写入本地 master 分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 回到 contrib 1
git checkout ec94292

# 创建临时分支
git checkout -b repair

# 获取 contrib 3
git cherry-pick 8b4eefb

git checkout master

# 删除 contrib 2 和 contrib 3
git reset --hard ec94292

# 写入 contrib 3
git rebase repair

# 清理临时分支
git branch -d repair

如果 cherry pick 的提交比较多,上面的方法就不如 rebase 那么方便了:

1
2
3
4
5
# 回到 master
git checkout master

# 交互式 rebase 至 contrib 1
git rebase -i ec94292

随后如法炮制,删除第一行 "pick a3ca1ef contrib 2" 即可。


注意事项

本文中涉及的 cherry-pick,rebase 和 reset 指令都具有危险性。在做出修改之前,请确保当前修改不会影响已经上传到服务器的历史。如果你不慎动了那些记录,将无法进行 git push 操作。在多数情况下,force push 是被禁止的,因为这会使其他所有协作者丢失他们未上传的代码。