Git学习笔记(2)-分支

什么是分支

在进行多个并行作用时,我们会用到分支,在这类并行开发的过程中,往往同时存在多个最新代码状态,如下所示:

在上图中,从master分支中创建了feature-A分支和fix-B分支后,每个分支中都拥有自己的最新代码,master分支是Git默认创建的分支,因此基本上所有的开发都是以这个分支为中心进行的。这种思路可以理解为,master不是一个树干,其他的分支就是树的枝叶。

在不同的分支中,可以同时进行完全不同的作业。等该分支的作用完成之后再与master合并,例如feature-A分支的作用结束后与master合并,如下所示:

Git中常见的分支命令

git branch显示分支一览表

git branch命令可以将分支列表显示,同时可以确认当前所在的分支,如下所示:

1
2
D:\zcode\test>git branch
* master

可以看到master的左侧有*标记,表示审 我们当前所在的分支,也就是说我们正在master分支下进行开发。结果中没有显示其他分支名,表示本地仓库中只存在master一个分支。

git checkout - b创建、切换分支

如果想以当前的master分支为基础创建新的分支,我们可以使用git checkout -b命令,现在我们创建一个名为feature-A的分支,如下所示:

1
git checkout -b feature-A

运行结果如下所示:

1
2
D:\zcode\test>git checkout -b feature-A
Switched to a new branch 'feature-A'

事实上,这条命令等同以下命:

1
2
git branch feature-A
git checkout feature-A

第一条命令git branch feature-A是创建一条名为feature-A的分支,git checkout feature-A则是切换到这条分支上。

现在运行git branch命令,如下所示:

1
2
3
D:\zcode\test>git branch
* feature-A
master

现在feature-A的左侧有一个星号,这说明当前分支是feature-A,在这个状态下,我们如果修改了代码,并执行git addgit commitgit push命令后,代码会提交至feature-A分支。像这种操作,也就是对像feature-A这样的分支进行提交的操作,称为培育分支

现在我们修改README.md文件的内容,如下所示:

1
2
3
# Git教程
- feature-A

然后提交,如下所示:

1
2
3
4
5
6
7
D:\zcode\test>git add README.md
warning: LF will be replaced by CRLF in README.md.
The file will have its original line endings in your working directory.
D:\zcode\test>git commit -m "Add feature-A"
[feature-A e3b591e] Add feature-A
1 file changed, 1 insertion(+), 1 deletion(-)

信息显示,修改后的文件已经添加到了feature-A分支上来。

切换到master分支上来

现在我们再切换到master分支上来,如下所示:

1
2
3
D:\zcode\test>git checkout master
Your branch is up-to-date with 'origin/master'.
Switched to branch 'master'

现在我们打开README.md文件,发现刚刚添加的文字已经消失了,也就是说,这个文件还保留着原先的状态。feature-A分支的更改不会影响到master分支,这就是在开发中创建分支的优点。

只要创建多个分支,就可以在不互相影响的情况下同时进行多个功能的开发。

切换到上一个分支

现在使用git checkout -命令切换回上一个分支,如下所示:

1
git checkout -

上面那种切换分支的命令是一个简写,也可以写为git checkout feature-A

分支操作

特性分支

特性分支是集中实现单一特性的分支,除此之外不进行任何作业的分支。在日常开发中,往往 会创建数个特性分支,同时在此之外再保留一个随时可以发布软件的稳定分支,稳定分支通常是由master分支担当,如下所示:

之前我们创建了feature-A分支,这一分支主要实现feature-A功能,除了feature-A的实现之外不进行任何作用,即使在开发的过程中发现了BUG,也需要再创建新的分支,在新分支中进行修正。当特定分支的代码完成后,再与master分支合并。只要保持这样一个开发流程,就能保证master分支可以随时供人查看。

主干分支

主干分支就是特性分支的原点,同时也是合并的终点,通常用户会使用master分支作为主干分支。主干分支中的代码都是完整代码,没有半成品,它是随时供他人查看的分支。有的时候,我们需要让这个主干分支总是配置在正式环境中,有时又需要用标签Tag等创建版本信息,同时管理多个版本𩑔,拥有多个版本发布时,主干分支也有多个。

git merge合并分支

假设现在我们已经实现了feature-A的功能,现在想要把它合并到主干分支master中,操作分为这几步:

第一,首先要切换到master分支,如下所示:

1
2
3
D:\zcode\test>git checkout master
Your branch is up-to-date with 'origin/master'.
Switched to branch 'master'

第二,合并feature-A分支,为了在历史记录中明确记录下本次分支合并,我们需要创建合并提交,在合并时加上--no-ff参数,如下所示:

1
2
3
4
D:\zcode\test>git merge --no-ff feature-A
Merge made by the 'recursive' strategy.
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

此时我们就可以看到,feature-A分支的内容就合并到master分支中了。

git log --graph图表查看分支

使用git log --graph可以看到feature-A分支的内容已经被合并,如下所示:

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
40
41
42
43
44
45
46
47
48
D:\zcode\test>git log --graph
* commit b08ecb501cf8ed84b86694161dd5459988d0cbd3
|\ Merge: a273f3a e3b591e
| | Author: 20170505a <app_web@qq.com>
| | Date: Thu Jul 4 15:03:41 2019 +0800
| |
| | Merge branch 'feature-A'
| |
| * commit e3b591e5a3421c94ff3c25b8a7338f0d86819d41
|/ Author: 20170505a <app_web@qq.com>
| Date: Thu Jul 4 14:50:45 2019 +0800
|
| Add feature-A
|
* commit a273f3ac1c1a120bfe2c6387c7109283caa74b89
| Author: 20170505a <app_web@qq.com>
:...skipping...
* commit b08ecb501cf8ed84b86694161dd5459988d0cbd3
|\ Merge: a273f3a e3b591e
| | Author: 20170505a <app_web@qq.com>
| | Date: Thu Jul 4 15:03:41 2019 +0800
| |
| | Merge branch 'feature-A'
| |
| * commit e3b591e5a3421c94ff3c25b8a7338f0d86819d41
|/ Author: 20170505a <app_web@qq.com>
| Date: Thu Jul 4 14:50:45 2019 +0800
|
| Add feature-A
|
* commit a273f3ac1c1a120bfe2c6387c7109283caa74b89
| Author: 20170505a <app_web@qq.com>
| Date: Thu Jul 4 13:35:19 2019 +0800
|
| Second
|
* commit 6b3a637cee900627058c4896b3330146d1ea4f58
| Author: 20170505a <app_web@qq.com>
| Date: Thu Jul 4 13:23:11 2019 +0800
|
| Add index
|
* commit 8d73594940e8a3a5be8a30db0fcc149019d8fb91
Author: 20170505a <app_web@qq.com>
Date: Thu Jul 4 13:07:18 2019 +0800
First commit
~

历史版本操作

Git可以很好地操作历史版本。

git reset回溯历史版本

git reset这个命令用于返回到以前的某个版本中,具体的命令为git reset --hard 哈希值

现在我们回溯到feature-A合并前的状态,先查一下合并前状态的哈希值,使用git log命令查看后,哈希值为a273f3ac1c1a120bfe2c6387c7109283caa74b89,运行命令如下所示

1
git reset --hard a273f3ac1c1a120bfe2c6387c7109283caa74b89

运行结果如下所示:

1
2
D:\zcode\test>git reset --hard a273f3ac1c1a120bfe2c6387c7109283caa74b89
HEAD is now at a273f3a Second

由于我们已经回溯到了特性分支(feature-A)创建前的状态,因此所有的文件都回到了指定哈希值对应的时间点上,README.md文件中的内容也回到了当前的状态。

现在创建另外一个特性分支fix-B,如下所示:

1
2
D:\zcode\test>git checkout -b fix-B
Switched to a new branch 'fix-B'

现在我们修改README.md文件中的内容,改为如下所示:

1
2
3
# Git教程
- fix-B

然后提交README.md文件,如下所示:

1
2
3
4
5
D:\zcode\test>git add README.md
D:\zcode\test>git commit -m "Fix B"
[fix-B 60ab808] Fix B
1 file changed, 1 insertion(+), 1 deletion(-)

此时我们的状态就如下所示:

现在我们要完成一个任务,这个任务图如下所示:

这个图表示的意思是:主干分支合并feature-A分支的修改后,又合并了fix-B的修改。

现在来完成这个任务:

第一步:恢复到feature-A分支合并后的状态。使用git log命令只能查看以当前状态为终点的历史日志。所以这里要使用git reflog命令,这是查看当前仓库的操作日志。在日志中找出回溯历史之前的哈希值,通过git reset --hard命令恢复到回溯历史前的状态。

现在执行git reflog命令,查看当前仓库执行过的操作日志,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
D:\zcode\test>git reflog
60ab808 HEAD@{0}: commit: Fix B
a273f3a HEAD@{1}: checkout: moving from master to fix-B
a273f3a HEAD@{2}: reset: moving to a273f3ac1c1a120bfe2c6387c7109283caa74b89
b08ecb5 HEAD@{3}: merge feature-A: Merge made by the 'recursive' strategy.
a273f3a HEAD@{4}: checkout: moving from feature-A to master
e3b591e HEAD@{5}: checkout: moving from master to feature-A
a273f3a HEAD@{6}: checkout: moving from feature-A to master
e3b591e HEAD@{7}: commit: Add feature-A
a273f3a HEAD@{8}: checkout: moving from master to feature-A
a273f3a HEAD@{9}: commit: Second
6b3a637 HEAD@{10}: commit: Add index
8d73594 HEAD@{11}: commit (initial): First commit

在这个日志中,我们可以看到commit、checkout、reset和merge等Git命令的执行记录,只要不进行Git的GC(Garbage Collection,垃圾回收),就可以通过日志随意调取近近期的历史状态,就像给时间机器指定了一个时间点,在过去未来中自由穿梭一样,即使开发者错误执行了Git操作,基本也都可以利用git reflog命令恢复到原先的状态。

从上面的日志中可以看到feature-A特性分支合并后的状态,也就是b08ecb5 HEAD@{3}: merge feature-A: Merge made by the 'recursive' strategy.这一行,它的哈希值是b08ecb5,现在将HEAD、暂存区、工作树恢复到这个时间点的状态,如下所示:

1
2
3
4
5
6
7
8
9
10
11
D:\zcode\test>git branch
feature-A
* fix-B
master
D:\zcode\test>git checkout master
Your branch is up-to-date with 'origin/master'.
Switched to branch 'master'
D:\zcode\test>git reset --hard b08ecb5
HEAD is now at b08ecb5 Merge branch 'feature-A'

现在恢复操作后的任务图就如下所示:

消除冲突

现在只要合并fix-B分支,就可以得到我们想到的状态,如下所示:

1
2
3
4
D:\zcode\test>git merge --no-ff fix-B
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

此时,系统提示我们文件README.md出现了冲突。系统在合并README.md文件时,feature-A分支更改的部分与本次想要合并的fix-B分支更改的部分发生了冲突。如果不解决冲突,就无法完成合并,因此需要打开README.md文件,解决这个冲突。

打开README.md文件,其内容如下所示:

1
2
3
4
5
6
7
# Git教程
<<<<<<< HEAD
- feature-A
=======
- fix-B
>>>>>>> fix-B

其中,=======以上的部分是当前HEAD的内容,以下的部分是要合并的fix-B分支中的内容。我们把这个文件的内容改为下面的这个样子,也就是最终要合并的样子,如下所示:

1
2
3
# Git教程
- feature-A
- fix-B

这种修正就可能让feature-A和fix-B的内空并存于文件之中,但是在实际的软件开发过中,往往要删除其中之一.

冲突解决后,再执行git addgit commit命令,如下所示:

1
2
3
4
D:\zcode\test>git add README.md
D:\zcode\test>git commit -m "Fix conflict"
[master 91939ae] Fix conflict

git commit --amend修改提交信息

上面的命令在进行提交时,使用的提交信息为Fix conflict。但它其实是fix-B的分支,此时可以修改这条提交信息,使用的命令是git commit --amend,此时会出现一个文本文件,内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Fix conflict
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Thu Jul 4 20:20:14 2019 +0800
#
# On branch master
# Your branch is ahead of 'origin/master' by 4 commits.
# (use "git push" to publish your local commits)
#
# Changes to be committed:
# modified: README.md
#

现在将最上面的Fix conflict这个提交信息更改一下,改为Merge branch 'fix-B',保存即可,此时会出现以下信息:

1
2
3
D:\zcode\test>git commit --amend
[master c503f43] Merge branch 'fix-B'
Date: Thu Jul 4 20:20:14 2019 +0800

使用git log --graph命令就能看到提交日志中的相应内容也已经被修改,如下所示:

1
2
3
4
5
6
7
8
D:\zcode\test>git log --graph
* commit c503f43995795ef28bc243aedd29537e6b1d26c4
|\ Merge: b08ecb5 60ab808
| | Author: 20170505a <app_web@qq.com>
| | Date: Thu Jul 4 20:20:14 2019 +0800
| |
| | Merge branch 'fix-B'
... 其它内容略...

压缩历史git rebase

在合并特性分支之前,如果发现已提交的内容中有些拼写错误,可以提交一个修改,然后将这个修改包含到前一个提交中,压缩成一个历史记录,这是一个使用Git的技巧,下面看这种技巧的使用过程。

第一,先创建一个新的分支,命令为feature-C,如下所示:

1
2
D:\zcode\test>git checkout -b feature-C
Switched to a new branch 'feature-C'

第二,在上面的命令中我们已经切换到了feature-C分支中,现在修改README.md文件,添加一行信息,这样总的信息如下所示:

1
2
3
4
# Git教程
- feature-A
- fix-B
- faeture-C

第三,提交这部分内容,此时我们使用git commit -am命令,这个集合相当于git add与git comit命令的总和,如下所示:

1
2
3
D:\zcode\test>git commit -am "Add feature-C"
[feature-C 0048425] Add feature-C
1 file changed, 2 insertions(+), 1 deletion(-)

现在我们想要修改README.md中的内容,因为里面的fraeture-C拼错了,直接打开README.md,修改错误,如下所示:

1
2
3
4
# Git教程
- feature-A
- fix-B
- feature-C

使用git diff来查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
D:\zcode\test>git diff
diff --git a/README.md b/README.md
index 3d27263..29f2cd4 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
# Git教程
- feature-A
- fix-B
-- faeture-C
\ No newline at end of file
+- feature-C
\ No newline at end of file

然后提交,如下所示:

1
2
3
D:\zcode\test>git commit -am "Fix typo"
[feature-C deed53c] Fix typo
1 file changed, 1 insertion(+), 1 deletion(-)

现在我们遇到这样的情形:

第一,首次提交时发现了README.md文件中有一处错误,我们随后修正了这个错误,又进行了提交;

第二,这些提交在系统里都有是历史记录的,但是这种拼写错误的历史记录没有什么意义,在后续查询历史记录中,我不想让最初提交了错误的README.md历史记录出现;

第三,总之就是,我要修改历史记录。

如何解决?

思路就是:将含有Fix typo修正的内容与前一次的提交合并,在历史记录中就合并为一次提交,因此要用到git rebase命令。

使用git rebase命令可以选定当前分支中包含HEAD(最新提交)在内的两个最新历史记录为对象,并在编辑器中打开,如下所示:

1
git rebase -i HEAD~2

上述命令运行后,就会出现一个打开的文本文件,内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pick 0048425 Add feature-C
pick deed53c Fix typo
# Rebase c503f43..deed53c onto c503f43 (2 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

此时,我们将deed53cFix typo的历史记录压缩到0048425Add feature-C里,操作如下:

deed53c左侧的pick字样删除,改为fixup

如下所示:

1
2
pick 0048425 Add feature-C
fixup deed53c Fix typo

保存并关闭文本文件,命令行出现以下提示信息:

1
2
D:\zcode\test>git rebase -i HEAD~2
Successfully rebased and updated refs/heads/feature-C.

系统提示rebase成功,也就是以前面两个提交作为对象,将Fix typo的内容合并到上一个提交的Add feature-C中,改写成了一个新的提交,现在查看一下日志,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
D:\zcode\test>git log --graph
* commit dc7c62eae25e570d11fe77642a173c45c52c5437
| Author: 20170505a <app_web@qq.com>
| Date: Thu Jul 4 20:29:57 2019 +0800
|
| Add feature-C
|
* commit c503f43995795ef28bc243aedd29537e6b1d26c4
|\ Merge: b08ecb5 60ab808
| | Author: 20170505a <app_web@qq.com>
| | Date: Thu Jul 4 20:20:14 2019 +0800
| |
| | Merge branch 'fix-B'
| |
| * commit 60ab808aeec5e579bf4d7c845f33a9c2edf90c23
| | Author: 20170505a <app_web@qq.com>
| | Date: Thu Jul 4 15:20:39 2019 +0800

此时,我们就相当于修改了提交的历史,也就是将包含Fix typo这个记录的提交给消除了。

此时,我们将feature-C合并到master分支上来,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
D:\zcode\test>git checkout master
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
Switched to branch 'master'
D:\zcode\test>git merge --no-ff feature-C
Merge made by the 'recursive' strategy.
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
D:\zcode\test>git branch
feature-A
feature-C
fix-B
* master

参考资料

  1. GitHub入门与实践.[日] 大塚弘记.支鹏浩/刘斌(译).人民邮电出版社.2015-7