git变基

x33g5p2x  于2021-12-30 转载在 其他  
字(4.2k)|赞(0)|评价(0)|浏览(331)

假设有这么一个场景,一个分支一共有50次提交,其中第31-40次的提交是一个新功能的代码,这10次提交如果放到一个新的分支上来开发会更好,但当时没想到。后来产品改需求了(for example…),这10次提交的代码要被废掉,这时候应该怎么处理呢?可以一个个地revert,但这种方法很不优雅。由这个问题引出本篇博客要讨论的主题:变基。git提供了两个命令可以很方便地来解决上面的问题,分别是git cherry-pickgit rebase

初始化一个场景

为了更快的进入状态,我写了一个linux脚本,来连续创建6个提交,为了使问题变得简单,每个提交只创建一个空文件,脚本的名字叫init.sh,内容如下

#!/bin/bash 
rm *.txt
rm -rf .git
git init
echo 'init.sh' > .gitignore
touch a.txt
git add a.txt .gitignore
git commit -m 'add a'
touch b.txt
git add b.txt
git commit -m 'add b'
touch c.txt
git add c.txt
git commit -m 'add c. bad commit'
touch d.txt
git add d.txt
git commit -m 'add d. bad commit'
touch e.txt
git add e.txt
git commit -m 'add e'
touch f.txt
git add f.txt
git commit -m 'add f'
git tag F HEAD
git tag E HEAD^
git tag D HEAD^^
git tag C HEAD~3
git tag B HEAD~4
git tag A HEAD~5
git log --oneline --decorate

脚本的最后一个命令是git log,会打印出当前的提交日志,内容如下

$ git log --oneline --decorate
46de330 (HEAD -> master, tag: F) add f
7e9135b (tag: E) add e
d65a486 (tag: D) add d. bad commit
eb3efc5 (tag: C) add c. bad commit
19ed1f8 (tag: B) add b
45d33f5 (tag: A) add a

给张图会更清晰

脚本中还分别给六次提交都加了tag,依次是A、B、C、D、E、F。我们要做的事情,是将中间两次提交(分别是C和D)干掉。

使用cherry-pick逐个修改

为了更好地理解变基,我们先用git cherry-pick命令来修改。这个命令就是将指定的一个commit的parentId指向HEAD指向的commit(如果觉得绕口,就想想linklist的删除过程),大概的思路就是先checkout到B,然后分别cherry-pick E和F,使他们形成一个链,A <-- B <-- E <-- F

checkout到B,HEAD指针就直接指向了这个提交

$ git checkout B
Note: checking out 'B'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 19ed1f8 add b

嫁接E和F

$ git cherry-pick E
[detached HEAD 1e75d3f] add e
 Date: Fri Apr 13 17:55:50 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 e.txt

此时,E的parent指针指向了B,然后HEAD又指向了E。继续执行cherry-pick

$ git cherry-pick F
[detached HEAD a1a4101] add f
 Date: Fri Apr 13 17:55:50 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 f.txt

此时,F的parent指针指向了E,而HEAD又指向了F。看一下图表

此时,这条新的连接线已经将C和D成功地移除了,但工作并没有完成,因为此时还是头指针分离的状态,下面要做两个比较重要的操作:1)checkout到master,退出头指针分离的状态,2)reset到刚才操作cherry-pick的连接线上。

$ git checkout master
Warning: you are leaving 2 commits behind, not connected to
any of your branches:

  a1a4101 add f
  1e75d3f add e

If you want to keep them by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> a1a4101

Switched to branch 'master'

退出头指针分离的状态后,再看一下图

WLGQ,刚才的修改到哪里去了?不要着急,git都记着呢,使用git reflog命令来看一下刚才的操作去哪里了

$ git reflog
46de330 (HEAD -> master, tag: F) HEAD@{0}: checkout: moving from a1a4101c0d06ee26f1d822bf9f12082d20aa39b8 to master
a1a4101 HEAD@{1}: cherry-pick: add f
1e75d3f HEAD@{2}: cherry-pick: add e
19ed1f8 (tag: B) HEAD@{3}: checkout: moving from master to B
46de330 (HEAD -> master, tag: F) HEAD@{4}: commit: add f
7e9135b (tag: E) HEAD@{5}: commit: add e
d65a486 (tag: D) HEAD@{6}: commit: add d. bad commit
eb3efc5 (tag: C) HEAD@{7}: commit: add c. bad commit
19ed1f8 (tag: B) HEAD@{8}: commit: add b
45d33f5 (tag: A) HEAD@{9}: commit (initial): add a

上面清楚地记着,在a1a4101 HEAD@{1}这一步,cherry pick到了F,接下来让master分支指向这里就好了

$ git reset --hard HEAD@{1}
HEAD is now at a1a4101 add f

再来看一下现在的状态

在看一下工作区的文件,发现c.txt和d.txt文件也消失了。

$ ls -l
total 8
-rw-r--r--  1 kite  wheel    0  4 13 17:55 a.txt
-rw-r--r--  1 kite  wheel    0  4 13 17:55 b.txt
-rw-r--r--  1 kite  wheel    0  4 16 10:26 e.txt
-rw-r--r--  1 kite  wheel    0  4 16 10:26 f.txt
-rwxr-xr-x  1 kite  wheel  527  4 13 17:35 init.sh

ok,成功地完成了变基,干掉了C、D两个提交,如果被干掉的代码还要继续使用,可以使用git checkout -b new-branch D命令,从D创建一个新的分支,C、D的代码都还在,可以在新分支上继续修改该功能。将来如果需要将这部分的代码发布出去的话,直接将这个分支合并到master分支就好了。

使用git rebase 一次完成

上面的例子中,C、D两个bad commit后面只有两个提交,我们可以使用git cherry-pick一步一步地重演了这两个提交,但如果bad commit后面已经有了200个提交,我们不可能一步一步地重演这200个提交,为了解决这个问题,可以使用git rebase命令,一次就能将后面的一段提交全部嫁接过来。

git rebase只能代替cherry-pick命令,后面的退出头指针分离状态和切换到rebase过的分支的工作还需要手动完成。

我们使用git rebase --onto的用法来完成这个事情,它的用法是

git rebase --onto <target-commit> <from-commit>(不含) <to-commit>

意思就是,将<from-commit> - <to-commit>这一段的提交,依次cherry-pick<target-commit>上。

先使用下面的命令恢复到变基以前的状态

$ git reset --hard F
HEAD is now at 46de330 add f

看一下当前的状态

然后直接执行rebase命令变基

$ git rebase --onto B E^ F
First, rewinding head to replay your work on top of it...
Applying: add e
Applying: add f

这一个命令就等于先checkout 到B,然后再执行若干次cherry-pick操作。

看一下状态

变基成功,并且此时git自动进入了头指针分离的状态。接下来要做的,跟使用cherry-pick一样,1)checkout到master,退出头指针分离的状态,2)reset到刚才操作git rebase的连接线上。

$ git checkout master
$ git reset --hard HEAD@{1}

关于git checkout命令和git reset命令的详细用法,可以参考我之前的博客

git reset (重置)

git checkout(检出)

相关文章