Git 分支与合并

内容纲要

查看【Git】专题可浏览更多内容

使用分支可以让我们从主线上分离开来,避免影响主线。

什么意思呢?想一想当都在主线上同时进行多个方向的工作,会有哪些问题:

  • 假设你是一位小说家,对于接下来的故事走向有两个方向,那么应该如何管理呢?
  • 假设你是一位工程师,领导先是要求加入购物车功能,但后来又改主意了,如何保留其他有用功能的代码的同时,只清除掉购物车功能的代码呢?
  • 在团队多人协作时,如何不影响到其他成员而进行自己的工作呢?

你可能会发现,尝试着在一个单一上下文背景环境中同时进行多个主题的工作时,事情只会变得越来越糟糕。

这些问题可以通过常见的 Git 分支的使用场景,或者说工作流解决:

  • 长期分支(Long-Running Branches)
    长期分支存于代在项目的整个生命周期,表了在整个项目生命周期的一种状态。以开发项目为例:将主分支定位为最高等级的分支,仅用于保存产品代码。如此一来主分支可以直接用来上线于生产环境,然后新建的开发分支(它衍生于主分支)专门用于开发、测试等等,待稳定后合并于主分支。
  • 主题分支(Topic Branches)
    主题分支就是基于某种主题的分支,例如着手开发一个新的功能的或是修复错误时,都应该对应不同的主题建立一个新的分支。这也是最常见的做法,特别是对于团队协作时你在本地的分支远程仓库并不可见,就可以尝试更多的方案择优选择,当多人协作时也可以避免提交冲突。

分支与合并

查看分支

在创建一个 Git 项目时,Git 就已经默认为我们创建了一条名为「master」的分支,还记得在使用 git status 命令时,返回结果第一行就在告诉我们当前位于什么分支吗?

位于分支 master
无文件要提交,干净的工作区

一般来说我们可以把默认创建的「master」当作主分支,也就是上面提到的「长期分支」。另外,也不要觉得这个「master」相比其他分支有什么特别之处,它跟后续创建的其他分支都一样,只是名字叫做「master」而已。

💡 因为🙄️政治正确,开始兴起使用「main」来替代「master」的运动,所以你可能在一些平台或项目看到「main」而不是「master」...

除了使用 git status 还可以使用 git branch 命令查看 Git 的分支:

# 查看本地分支
git branch

# 显示每个分支最新的 Commit ID 和提交信息
git branch -v

# 显示所有远程和本地的分支:
git branch -a

创建分支

假设当前只有一个分支 master,里面只有一个文稿文件 README.md,它的内容是这样的:

Git 简史

同生活中的许多伟大事物一样,Git 诞生于一个极富纷争大举创新的年代.

你接到一个「编号为 3」 的反馈说该项目的缺乏格式以及标点符号是英文符号,需要修改。

那开始着手进行修改吧,首先创建一个名为 iss3 的分支:

# git branch <branch-name>
git branch iss3

如果想要创建并立即切换到所创建的分支可以这样:

# 创建名为 iss3 的分支并切换过去
git switch -c iss3 # 在旧版本的 Git 中普遍使用 checkout,如:git checkout -b iss3

切换分支

切换到指定分支可以使用 git switch 命令:

# 切换至 iss3 分支
git switch iss3

# 如果想快速从哪来回哪去,如从 master 切换到 iss3 后想快速回 master,可以使用 -
git switch - # 在旧版本 Git 中普遍使用 git checkout master,注意这里没有 -b 选项

合并分支

切换到 iss3 后提交了两次提交,将 README.md 进行修改成了这样:

## Git 简史

同生活中的许多伟大事物一样,Git 诞生于一个极富纷争大举创新的年代。

两次提交为:

  1. 修正格式 - 第一行的修改,加上了标题
  2. 修正标点符号 - 第二杭的修改,使用中文标点;

修改完成后,将修改内容合并到 master 分支:

# 首先切换回 master 分支
git switch master

此时 master 分支下的 README.md 文件还是未修改的,现在合并指定分支 iss3 到当前分支 master

git merge iss3

删除分支

合并后 iss3 分支就可以删除了

# 删除已合并分支
# git branch -d <branch-name>
git branch -d iss3

此外,对于没有合并的分支,删除时可以使用大写的 -D 选项

合并的几种方式

和之前一样,假设当前 master 分支有了两次提交,现在接到一个问题报告,需要修正一下提交内容,所以创建了一个名为 iss3 的分支,然后分别做了两次提交 修正格式修正标点符号

然后看一下几种合并方式的不同:

fast-forward

# git merge <branch-name>
git merge iss3
更新 d868ecb..4ed557f
Fast-forward
 READMD.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

显示使用了 Fast-forward

然后看下历史记录:

git log --graph --pretty=short
* commit 4ed557f63f84b07db6e12c5cde9027842a86e574 (HEAD -> master, iss3)
| Author: Example.com <[email protected]>
|
|     修正标点符号
|
* commit eda8f8376c18e2418431b6d750a96dcf1e03d361
| Author: Example.com <[email protected]>
|
|     修正格式
|
* commit d868ecb35f367e5fa04df8e4d6b74a26c0122768
| Author: Example.com <[email protected]>
|
|     添加初稿
|
* commit 0467ce9b6ec136599ecbe6d780e4ba33b6e97610
  Author: Example.com <[email protected]>

      项目成立

可以看到 master 分支的提交历史中也有 iss3 的提交历史,并且注意左侧的支线是一条直线。

--no-ff

# git merge --no-ff <branch-name>
git merge --no-ff iss3
Merge made by the 'ort' strategy.
 READMD.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

可以看到显示 Merge made by the 'ort' strategy.ort 是旧版 Git 中的 recursive 策略的重构,解决了一些功能问题和性能问题。

然后看下历史记录:

git log --graph --pretty=short
*   commit 4bfdc9dce575b0d674b431c00baff5a1fccdd79d (HEAD -> master)
|\  Merge: d868ecb 4ed557f
| | Author: Example.com <[email protected]>
| |
| |     Merge branch 'iss3'
| |
| * commit 4ed557f63f84b07db6e12c5cde9027842a86e574 (iss3)
| | Author: Example.com <[email protected]>
| |
| |     修正标点符号
| |
| * commit eda8f8376c18e2418431b6d750a96dcf1e03d361
|/  Author: Example.com <[email protected]>
|
|       修正格式
|
* commit d868ecb35f367e5fa04df8e4d6b74a26c0122768
| Author: Example.com <[email protected]>
|
|     添加初稿
|
* commit 0467ce9b6ec136599ecbe6d780e4ba33b6e97610
  Author: Example.com <[email protected]>

      项目成立

可以看到,使用 --no-ffmaster 的提交历史虽然可以看到 iss3 的提交历史,但是注意最左侧的线,它清晰的显示这部分的提交是来自于另一条分支。

另外,如果在 iss3 分支工作期间,master 分支出现了新的提交,那么在之后合并 iss3 分支使,即便使用 fast-forward 进行合并,Git 也将使用 ort 策略。

--squash

# git merge --squash <branch-name>
git merge --squash iss3
更新 d868ecb..4ed557f
Fast-forward
挤压提交 -- 未更新 HEAD
 READMD.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

如果合并时使用的是 --squash 那么使用命令 git log --graph --pretty=short 时你会发现没有新的提交,因为此时来自 iss3 分支的修改内容直接存到 master 分支的工作区和暂存区。

这时候需要手动的提交:

git commit -m '合并 iss3 分支,修正文稿内容'

然后再查看下历史

git log --graph --pretty=short
* commit c7969b6448315be5c9e86858dcbcf056d4be997c (HEAD -> master)
| Author: Example.com <[email protected]>
|
|     合并 iss3 分支,修正文稿内容
|
* commit d868ecb35f367e5fa04df8e4d6b74a26c0122768
| Author: Example.com <[email protected]>
|
|     添加初稿
|
* commit 0467ce9b6ec136599ecbe6d780e4ba33b6e97610
  Author: Example.com <[email protected]>

      项目成立

使用 --squash 可以让其他分支的最新改动直接存到当前分支的工作区和暂存区,然后再进行提交,并且不会显示被合并分支的提交历史。

HEAD 与 detached HEAD

一般来说 HEAD 指向当前分支的最新提交,每一次提交,HEAD 都向前移动一位,在使用 git log 时会告诉你 HEAD 指向的位置,如:

commit a1c313d85cbc517aa37b5ada8a553ce65eb8e91f (HEAD -> master)

在旧版本的 Git 中,git checkout 承担了很多任务,例如切换分支:

git checkout master

这样就切换到了指定分支,且 HEAD 指向该分支的最新提交。

除了对于分支的操作还可以针对 Commit 操作,如:

# git checkout <commit-id>
git checkout d9e69
注意:正在切换到 'd9e69'。

您正处于分离头指针状态。您可以查看、做试验性的修改及提交,并且您可以在切换回一个分支时,丢弃在此状态下所做的提交而不对分支造成影响。

如果您想要通过创建分支来保留在此状态下所做的提交,您可以通过在 switch 命令中添加参数 -c 来实现(现在或稍后)。例如:

  git switch -c <新分支名>

或者撤销此操作:

  git switch -

通过将配置变量 advice.detachedHead 设置为 false 来关闭此建议

HEAD 目前位于 d9e694d 添加了标点符号

使用命令 git branch 也会看到相应提示:

* (头指针在 d9e694d 分离)
  master

此时就出现了「分离头指针 (detached HEAD)」的情况,也就是 HEAD 不指向任何分支而是某一次提交。

❓ 那么为什么需要 git checkout <commit-id> 来「检出」某一次提交呢?

有的时候你可能只是突发奇想查看某次提交以及在这次提交上做一些实验,并没有真的想好所以也就没有专门新建一个分支来处理,就可以「检出」某次提交进行一些改动:

  • 如果这次实验的改动不满意,使用 git switch - 切回来时的分支就让 HEAD 回到分支的最新提交;
  • 如果这次实验的改动满意,可以先进行提交,然后使用命令创建一条新分支,如 git switch -c test (在旧版本 Git 中普遍使用 git checkout -b <分支名> 来创建),此时的 HEAD 就是新建分支的最新提交;

    在提交后如果没有创建分支而切换到了其他分支将会看到这样的提示:

警告:您正丢下 1 个提交,未和任何分支关联:

  fa5dc59 test

如果您想要通过创建新分支保存它,这可能是一个好时候。
如下操作:

 git branch <新分支名> fa5dc59

切换到分支 'master'

此时如果需要保存这次实验改动,就可以使用命令 git branch test fa5dc59 (test 是新分支名),如果一直不处理 Git 将定期清理这些提交。

那么如果想要将这份改动合并到如 master 怎么做呢?切换到 master 分支后使用命令:git merge test