LV01-Git-04-Git本地仓库-09-rebase

本文主要是Git中git rebase的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
点击查看本文参考资料
点击查看相关文件下载
--- ---

【说明】本节笔记的相关操作在Windows下进行,因为VS Code有个Git的插件,可以很直观的演示一些东西。由于Git安装后自带一个Git-Bash终端,所以就不用Win下的命令行啦,就用的这个终端,因为它里边的命令与Linux很类似,而windows中的命令行有些命令与linux并不相同,为了统一,还是用用Git自带的终端啦。另外VS Code是可以选择使用的终端的,我直接将VS Code使用的终端改成了git-bash,这样更方便一些。

在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。 我们将学习什么是“变基”,怎样使用“变基”,并将展示该操作的惊艳之处,以及指出在何种情况下应避免使用它。

一、分叉的提交历史

1. 提交历史分叉的情况

前边学习分支的合并的时候,我们知道这样的仓库情况,在进行合并之后,会产生分叉:

image-20230623173710631

2. 仓库准备

2.1 如何产生分叉

上边的图是怎么产生的呢?需要经过以下步骤:

(1)master分支进行C0、C1和C2三次提交;

(2)创建dev分支,并切换到dev分支,此时dev分支从C2开始,然后在此分支进行修改,然后提交一次C4;

(3)切换到master,在C2之后,提交一次C3;

经过上边的这几个步骤之后,我们通过git merge合并的话,就会产生分叉。

2.2 分叉的分支准备

2.2.1 到C2时master分支的main.c

点击查看main.c文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <unistd.h>

// C0功能实现
void func_C0(void)
{
printf("C0功能开始执行!\n");
printf("C0>>>>>>步骤!\n");
printf("C0功能执行完毕!\n");
}

// C1功能实现
void func_C1(void)
{
printf("C1功能开始执行!\n");
printf("C1>>>>>>步骤!\n");
printf("C1功能执行完毕!\n");
}

// C2功能实现
void func_C2(void)
{
printf("C2功能开始执行!\n");
printf("C2>>>>>>步骤!\n");
printf("C2功能执行完毕!\n");
}

int main(int argc, const char *argv[])
{
/* code */
while (1)
{
func_C0(); // 实现C0功能
func_C1(); // 实现C1功能
func_C2(); // 实现C2功能
sleep(1);
}
return 0;
}
点击查看提交记录
1
2
3
4
$ git mylog
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (HEAD -> master) (2023-06-23 17:44:59)
* [qidaink] da60a44 : feat:C1(实现C1功能并提交) (2023-06-23 17:44:34)
* [qidaink] 3614b67 : feat:C0(实现C0功能并提交) (2023-06-23 17:43:58)

2.2.4 新建dev分支并提交

  • 新建并切换到dev分支
1
git checkout -b dev
  • 修改文件内容
image-20230623175457017
  • 提交C4修改
1
git commit -a -m "feat:C4(dev分支修改C0 C1 C2功能并提交)"
  • 查看提交记录
1
2
3
4
5
$ git mylog 
* [qidaink] 7aebfbd : feat:C4(dev分支修改C0 C1 C2功能并提交) (HEAD -> dev) (2023-06-23 17:55:58)
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (master) (2023-06-23 17:44:59)
* [qidaink] da60a44 : feat:C1(实现C1功能并提交) (2023-06-23 17:44:34)
* [qidaink] 3614b67 : feat:C0(实现C0功能并提交) (2023-06-23 17:43:58)

2.2.3 切换到master分支并提交

  • 切换到master分支
1
git checkout master
  • 修改文件内容
image-20230623175805573
  • 提交C3修改
1
git commit -a -m "feat:C3(master分支修改C0 C1 C2功能并提交)"
  • 查看提交记录
1
2
3
4
5
$ git mylog 
* [qidaink] 918dcbf : feat:C3(master分支修改C0 C1 C2功能并提交) (HEAD -> master) (2023-06-23 17:58:46)
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (2023-06-23 17:44:59)
* [qidaink] da60a44 : feat:C1(实现C1功能并提交) (2023-06-23 17:44:34)
* [qidaink] 3614b67 : feat:C0(实现C0功能并提交) (2023-06-23 17:43:58)

3. 分叉产生

我们此时切换到master分支,然后将dev分支的内容合并到master分支们就会产生分叉:

1
2
3
4
5
6
$ git checkout master
Already on 'master'
$ git merge dev
Auto-merging main.c
CONFLICT (content): Merge conflict in main.c
Automatic merge failed; fix conflicts and then commit the result.

这种情况下一定会产生冲突,这里的重点不在解决冲突,我们迅速处理一下,直接提交:

1
git commit -a -m "feat:C5(dev分支合并到master分支,解决冲突后提交)"

这个时候我们看一下提交记录的ASCII图:

1
git log --graph --oneline --decorate
image-20230623180830638

这个时候果然产生了分叉,整个版本库当前的情况如下:

image-20230623181113883

4. master分支回到未合并的时候

我们后边要学习变基,所以这里需要恢复到未合并dev分支的时候:

1
git reset --hard 918dcbf

然后再查看master分支提交记录如下:

1
2
3
4
5
$ git mylog 
* [qidaink] 918dcbf : feat:C3(master分支修改C0 C1 C2功能并提交) (HEAD -> master) (2023-06-23 17:58:46)
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (2023-06-23 17:44:59)
* [qidaink] da60a44 : feat:C1(实现C1功能并提交) (2023-06-23 17:44:34)
* [qidaink] 3614b67 : feat:C0(实现C0功能并提交) (2023-06-23 17:43:58)

二、变基基本操作

我们还可以提取dev分支在 C4 中引入的补丁和修改,然后在master分支的 C3 的基础上应用一次,也就是说将 dev 分支 C4 中所作的修改直接添加到 master 分支的 C3 中。 在 Git 中,这种操作就叫做 变基(rebase)。 我们可以使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

1. 变基操作

1.1 切换到dev分支

我们首先切换到dev分支:

1
2
$ git checkout dev
Switched to branch 'dev'

此时仓库情况如下:

image-20230623181825490

1.2 变基到master

我们执行以下命令,进行变基:

1
git rebase master

然后会得到以下输出信息:

image-20230623182109610

看起来还是在报错,我们看一下当前所有文件的状态以及所处的分支:

1
2
git branch
git status
image-20230623182233510

可以看到当前是处于一个自动产生的分支并不属于master,也不属于dev,并且文件是发生了变化的,我们通过VS Code查看一下文件发生了哪些变化:

image-20230623182419147

可以看到其实就跟我们使用git merge的时候一样,产生了冲突。

1.3 解决冲突并提交

我们处理一下冲突,然后提交一次:

1
git commit -a -m "feat:C4'(dev分支变基到master,解决冲突后提交)"

然后我们查看一下当前所处分支还有提交记录:

image-20230623182927118

发现还处于那个中间分支,并没有回到我们的master分支或者dev分支。

1.4 继续变基

还记得前边的报错吧,报错提示信息如下:

1
2
3
4
5
6
7
8
9
$ git rebase master
Auto-merging main.c
CONFLICT (content): Merge conflict in main.c
error: could not apply 7aebfbd... feat:C4(dev分支修改C0 C1 C2功能并提交)
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 7aebfbd... feat:C4(dev分支修改C0 C1 C2功能并提交)

这里其实已经提示我们了,我们有三个选择:

1
2
3
git rebase --continue # 继续变基
git rebase --abort # 回到 git rebase 之前的状态
git rebase --skip # 跳过这个提交

我们这里选择继续git rebase --continue,然后会看到有如下信息:

1
2
$ git rebase --continue 
Successfully rebased and updated refs/heads/dev.

此时我们看一下当前所处的分支及提交记录:

image-20230623184406589

可以看到,中间分支消失了,我们回到了dev分支,然后之前dev分支的C4提交记录消失了,取而代之的是另外两条提交记录(只写发生变化的记录):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 原来的dev提交记录
* [qidaink] 7aebfbd : feat:C4(dev分支修改C0 C1 C2功能并提交) (HEAD -> dev) (2023-06-23 17:55:58)
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (master) (2023-06-23 17:44:59)
// 剩下的几条省略......

# 变基之后的dev提交记录
* [qidaink] 463111a : feat:C4'(在dev分支变基到master,解决冲突后提交) (HEAD -> dev) (2023-06-23 18:42:43)
* [qidaink] 918dcbf : feat:C3(master分支修改C0 C1 C2功能并提交) (master) (2023-06-23 17:58:46)
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (master) (2023-06-23 17:44:59)
// 剩下的几条省略......

# master分支的提交记录
* [qidaink] 918dcbf : feat:C3(master分支修改C0 C1 C2功能并提交) (HEAD -> master) (2023-06-23 17:58:46)
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (2023-06-23 17:44:59)
// 剩下的几条省略......

1.5 dev分支差异分析

我们来分析一下dev分支改变的那两次提交都改变了什么。

  • (1)dev分支中C2和C3提交
1
git difftool 7e54c61 918dcbf -y
image-20230623185241501

分析一下,这不就是我们master在C2之后的那次提交嘛。

  • (2)dev分支中C3与C4‘提交
1
git difftool 918dcbf 463111a -y
image-20230623185451988

这不就是之前我们在dev分支C2之后提交的C4的差异嘛。

1.6 快进合并

经过上边的更改,发现了吗,dev分支似乎就是从master分支的C3提交之后新建的分支一样,这样不就相当于我们master分支开发到了C3,然后新建了分支dev,并在dev分支进行了新的提交,这个时候,我们直接merge,不就可以实现dev的修改合并到master了吗?我们执行以下命令:

1
2
3
4
5
6
7
8
$ git checkout master
Switched to branch 'master'

$ git merge dev
Updating 918dcbf..463111a
Fast-forward
main.c | 3 +++
1 file changed, 3 insertions(+)

1.7 查看master提交记录

我们来查看一下master分支的提交记录:

1
git log --graph --oneline --decorate
image-20230623185954097

我们发现此时这个分支没有分叉了,似乎变得很简洁,一条线很清楚。

2. 原理分析

2.1 变基过程中

git rebase的原理是首先找到这两个分支(即当前分支 dev、变基操作的目标基底分支 master) 的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。

image-20230623190618678

经过前边的过程,我们知道在变基过程中,也就是敲完git rebase master之后,产生了一个中间分支,叫(no branch, rebasing dev),在次临时分支中,整合了dev分支中的C4的修改和master分支的C3的修改,由于修改的同一行,所以产生了冲突,我们在此临时分支上解决冲突,并提交了C4’。

这一步由于在整合两个分支的不同之处,所以,看起来更像是在C3的基础上加上了C4的修改。

2.2 变基完成

当我们敲下git rebase continue的时候,完成了变基,此时,dev分支就变成了刚才的临时分支,包含了C4’提交:

image-20230623191149027

2.3 快进合并

变基完成后,dev分支就像是从master分支的C3处新建分支,然后进行了C4’提交,这样的话,我们在master分支直接快进合并一次,就可以迅速将mater更新到C4’了:

image-20230623191614200

此时,C4' 指向的快照就和前边分叉的时候的 C5 指向的快照一模一样了。 这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。

我们在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,我们首先在自己的分支里进行开发,当开发完成时你需要先将我们的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

三、合并提交记录

git rebase命令不仅可以用与变基,使提交记录不产生分叉,还可以合并提交记录。

1. 情况说明

rebase 可以让提交历史变得简洁,试想一下,一个功能的开发需要将近一个月的时间才能完成,在这一个月中不可能一次都不提交,这样提交的多了,提交记录就会显得特别的冗杂,这可不是大家希望看到的,这时候就会想,要是提交记录能合并该多好,于是 Git 中的 rebease 就为这种想法提供了可能。

2. 命令说明

1
2
3
4
5
# 合并从当前开始的前n个记录(包括当前)
git rebase -i HEAD~n # n 为要合并的提交记录数

# 合并指定版本号(不包含此版本)
git rebase -i [commit_id]
  • -i(--interactive):弹出交互式的界面进行编辑合并
  • [commit_id]:要合并多个版本之前的版本号,注意:[commit_id] 本身不参与合并,就相当于合并(commit_id, HEAD]这个区间内的提交记录。

3. 编辑器中的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 命令:
# p, pick <提交> == 使用提交
# r, reword <提交> == 使用提交,但编辑提交说明
# e, edit <提交> == 使用提交,但停止以便在 shell 中修补提交
# s, squash <提交> == 使用提交,但挤压到前一个提交
# f, fixup [-C | -c] <提交> == 类似于 "squash",但只保留前一个提交
# 的提交说明,除非使用了 -C 参数,此情况下则只
# 保留本提交说明。使用 -c 和 -C 类似,但会打开
# 编辑器修改提交说明
# x, exec <命令> == 使用 shell 运行命令(此行剩余部分)
# b, break = 在此处停止(使用 'git rebase --continue' 继续变基)
# d, drop <提交> = 删除提交
# l, label <label> = 为当前 HEAD 打上标记
# t, reset <label> = 重置 HEAD 到该标记
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . 创建一个合并提交,并使用原始的合并提交说明(如果没有指定
# . 原始提交,使用注释部分的 oneline 作为提交说明)。使用
# . -c <提交> 可以编辑提交说明。
#
# 可以对这些行重新排序,将从上至下执行。
#
# 如果您在这里删除一行,对应的提交将会丢失。
#
# 然而,如果您删除全部内容,变基操作将会终止。
#

【注意】

(1)至少要有三次提交时才可以进行提交记录合并,也就是说,其他的提交记录不能与第一条提交记录合并,否则会报:fatal: 无效的上游 ‘HEAD~2’

(2)在进入编辑器后,第一行的 pick 不要修改,其他的 pick 要改为 s ,这样提交记录才能进行合并,若是全部改为 s 则会报以下问题,出现该问题时,可以先终止,再重新合并。

4. 使用实例

4.1 查看提交记录

我们用前边变基的仓库就好啦,我们来看一下master的提交记录:

1
2
3
4
5
6
$ git mylog 
* [qidaink] 463111a : feat:C4'(在dev分支变基到master,解决冲突后提交) (HEAD -> master, dev) (2023-06-23 18:42:43)
* [qidaink] 918dcbf : feat:C3(master分支修改C0 C1 C2功能并提交) (2023-06-23 17:58:46)
* [qidaink] 7e54c61 : feat:C2(实现C2功能并提交) (2023-06-23 17:44:59)
* [qidaink] da60a44 : feat:C1(实现C1功能并提交) (2023-06-23 17:44:34)
* [qidaink] 3614b67 : feat:C0(实现C0功能并提交) (2023-06-23 17:43:58)

4.2 合并最新3条提交记录

我们现在将C2、C3和C4’这三条提交记录合并成一条,可以输入以下命令:

1
git rebase -i HEAD~3

然后变回打开git默认的编辑器,交互式的让我们进行操作:

image-20230623214231762

我们保留第一个pick,将后边两行的pick改为s,改为s就表示将本次提交合并到前一个提交,然后保存,退出编辑器。

4.3 修改合并后的提交说明

上一步完成后,会再次进入编辑器编辑模式,这一次是修改合并后需要显示的提交说明,这里没有#的部分将会作为我们合并后的提交说明,我们可以不修改直接提交,也可以注释掉重新写一个。

image-20230623214435924

我们将上边的三条记录修改成如下所示:

1
feat:C2~C4'合并后的提交

然后保存退出编辑器即可。

4.4 查看提交记录

我们输入查看提交记录的命令,可以看到提交记录已经被合并成1条了。

image-20230623214644527