第三章:Git的分支管理

GIT的分支管理

一 分支介绍

​ 如果把Git比喻为一条小河,master分支是小河的主干/主分支,其他分支就是这条小河的支流,可以把所有分支合并到一起形成一条大河,滚滚奔向大海……

​ Git本质上只是一棵巨大的文件树,分支指的是树的一个分叉,我们可以对分支进行合并。

​ 那么分支在实际开发中有什么用处呢?

​ 如果你准备开发一个新功能,需要两周才能完成,第一周你写了50%的代码,此时如果立刻提交,由于代码还没写完,而不完整的代码库会导致别人不能干活了。但如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

​ 现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

​ 分支是Git的杀手级特征,而且Git鼓励在工作流程中频繁使用分支与合并,哪怕一天之内进行许多次都没有关系。因为Git分支非常轻量级,不像其他的版本控制,创建分支意味着要把项目完整的拷贝一份,而Git创建分支是在瞬间完成的,而与你工程的复杂程度无关。

Git理论扩展阅读:https://www.bookstack.cn/read/the-core-conception-of-git/0.md

二 创建分支与合并分支

​ 分支可以被理解为一条时间线,这一条时间线上串着一个个节点,每个节点都是一次提交记录/版本。

​ git默认只有一条时间线or分支,该时间线or分支称之为主分支,即master分支,文件.git/refs/heads/master 内存放着指向master分支最新版本的指针,如果下所示红色块即master指针,指向master分支

# 注意箭头方向,因为每一次提交都有一个指向上一次提交的指针,所以箭头方向向左,更为合理

每次提交,master分支都会向前移动一步,master指针指向最新提交的一个版本,这样,随着你不断提交,master分支的线也越来越长,如下

如果我们有多个分支,那么GIT如何实现在多个分支之间来回切换的呢,.git文件夹下的HEAD文件负责存放当前所在的分支ref: refs/heads/<分支名称>,切换分支就是修改HEAD文件中的内容为对应分支,并将工作区的文件恢复至对应分支的内容,所以严格来说HEAD不是指向提交,而是指向当前所在的分支,比如master,master才是指向提交的,一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:

当我们创建并切换到一个新的分支,例如dev

  • 1 、Git会新建一个指针叫dev,指向master相同的提交
  • 2、然后再把HEAD指向dev,就表示当前分支在dev
  • ps:Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!

如果HEAD指向dev,那么代表当前分支在dev上,这意味着接下来对工作的修改和提交都是针对dev分支了,如果master是小河的主干,那么dev就是这条河的支流,主干的水被引入了支流.

比如提交一次后,dev指针往后移动一步,而master指针不变

当在master分支合并dev分支时,因为他们在一条线上,这种单线的历史分支不存在任何需要解决的分歧,所以只需要master指针指向dev分支的最新一次提交即可,所以非常快。

所以你看,git不仅创建分支快,合并分支也很快!就改改指针,工作区内容也不变!

合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支,甚至都看不出来添加过dev分支,毁尸灭迹了

实操

  • 1、在当前分支下创建子分支并切换

    # 1.1 创建+切换=>两条命令搞定
    git branch dev # 在当前分支下创建子分支
    git checkout dev # 由当前分支切换到子分支
    
    #1.2 创建+切换=>一条命令搞定
    git checkout -b dev # 等同于上述两条命令,代表创建并切换到子分支
  • 2、直接切换到已有分支

    git checkout dev
  • 3、查看当前分支

    $ git branch # 该命令会列出所有分支,当前所在分支前会标一个*号。
    * dev
    master
  • 4、切换到dev分支下修改并提交记录/版本

    git checkout dev
    echo "111" > c.txt
    git add .
    git commit -m "v4"
    
    git checkout master
    git merge dev  # git merge命令用于合并指定分支到当前分支。

    如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward),上述合并git merge dev用的就是Fast-forward快进模式,毫无疑问不是每次合并都能采用Fast-forward,我们将在第四小节介绍。

    合并完成后,就可以放心地删除dev分支了

    $ git branch -d dev
    Deleted branch dev (was f1d9621).
    
    $ git branch
    * master

    因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。

  • 5、switch

    因为get checkout -- 文件通常是用来撤销修改的,又用它来创建并切换分支,有点容易让人混淆,所以
    最新版本的Git提供了新的git switch命令来创建or切换分支,推荐使用
    $ git switch -c dev  # 创建并切换到新的dev分支,等同于git checkout -b dev
    $ git switch master  # 直接切换到已有的master分支

三 分支管理策略

Fast-forward快进模式下合并的结果如下图所示

该模式下,删除分支dev后,查看版本日志根本看不到做过合并

git branch -d dev

如果想在删掉分支后,也能在版本日志里看到做过合并操作,需要在git merge时强制禁用Fast forward模式,并且提交一个的版本

$ git checkout -b dev
$ echo "haha" > readme.txt
$ git add .
$ git commit -m "v5"
$ git checkout master
$ git  merge --no-ff -m "no-ff合并版" dev  # --no-ff参数,表示禁用Fast forward;因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。

$ git log --graph --pretty=oneline --abbrev-commit
*   40656a7 (HEAD -> master) no-ff合并版
|\  
| * 1d0a989 (dev) v5
|/  
* a918dfc v5
* f1d9621 v4
* 3522291 v3
* b27998a v2
* 8519849 v1
* 34fe61e v0

如果merge时使用Fast forward模式

如果mere时禁用Fast forward模式,会提交一个新的版本

删除分支dev后查看

$ git branch -d dev # 删掉分支dev,仍然可以看到合并信息

$ git log --graph --pretty=oneline --abbrev-commit
*   40656a7 (HEAD -> master) no-ff合并版
|\  
| * 1d0a989 v5
|/  
* a918dfc v5
* f1d9621 v4
* 3522291 v3
* b27998a v2
* 8519849 v1
* 34fe61e v0

分支策略

在实际开发中,我们应该按照几个基本原则进行分支管理:

首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;

那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;

你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

所以,团队合作的分支看起来就像这样:

小结

Git分支十分强大,在团队开发中应该充分应用。

合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。

四 解决冲突

承接第二小节:如果顺着一个分支走下去可以到达另一个分支,中间无分叉的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)

但如果合并版本时有冲突问题,便不可用快进模式,当分支出现分叉时,就有可能出现冲突,而这时Git就会要求你去解决冲突,比如像下面的历史

分支和dev分支不在一条线上,即v7不是v5的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(v7v5)以及它们的共同祖先(v3)进行一次简单的三方合并计算。合并之后会生成一个和并提交v8

注意:合并提交有两个祖先(v7v5)。

#1、在主分支上连续提交了4个版本
git init 
git config --local user.name 'egon'
git config --local user.email 'egon@example.com'

echo "hello egon" > a.txt
git add .
git commit -m "v0"

echo "bbb" > b.txt
git add .
git commit -m "v1"

echo "ccc" > c.txt
git add .
git commit -m "v2"

echo "ddd" > d.txt
git add .
git commit -m "v3"

#2、切换到dev分支上修改a.txt的内容并提交1个版本
git checkout -b dev
vim a.txt # 改成hello sb
git add .
git commit -m "v4"

#3、切回master分支上修改a.txt的内容并提交1个版本,注意此时修改的a.txt与2中修改的发生了冲突
git checkout master
vim a.txt # 改成hi bigsb
git add .
git commit -m "v5"

#4、在主分支上合并dev分支,报错:冲突
$ git merge dev 
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
Automatic merge failed; fix conflicts and then commit the result.

#5、需要手动解决冲突
$ git status # 根据查看到冲突信息,确定是因为重复修改了a.txt同一行内容导致的

$ vim a.txt  # 手动解决冲突

$ git add a.txt # 再次提交
$ git commit -m "conflict fixed" # 再次提交

# 6、查看
$  git log --graph --pretty=oneline --abbrev-commit
*   cb8e739 (HEAD -> master) conflict fixed
|\  
| * d8dba1f (dev) v4
* | 5e6a978 v5
|/  
* 89e82b8 v3
* 68d496d v2
* 04352c4 v1
* 1452eed v0

# 7、最后删除dev分支
git branch -d dev

# 8、总结
当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。

解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。

用git log --graph命令可以看到分支合并图。

五 Bug分支

​ 开发软件的过程中,软件出现bug是非常正常的一件事,当我们在开发过程中遇到了bug,可以在出现bug的分支上创建并切换到一个临时的子分支,可以称之为bug分支,然后在该临时子分支上修复bug并提交,随后切回出现bug的分支上合并bug分支修复的内容,然后删掉临时的bug分支即可。

​ 具体场景。。。

​ 此时我们正在dev分支上进行开发,开发工作只进行了一半,还需要几天才能搞定,此时领导交给你一个代号487的bug修复的任务,要求在两个小时内修复完成,此时你必须放下手头dev分支的开发任务去修复bug,此时dev分支未完成的工作肯定是需要保存下来以便bug修复完成后重新继续开发下去的,如果把dev分支未完成的开发内容直接提交一个版本肯定是可以的,但我们通常是在开发完成一个任务后才去提交一个版本,你只写一行代码就提交一个版本,那岂不是非常乱,所以我们的需求是想在不提交新版本的前提下把开发的内容暂存下来,这就用到了git stash命令保存和恢复工作进度

前提:必须是处于git下的文件,从来未曾add到git的文件无法使用。
快速掌握

stash用于将工作区发生变化的所有文件获取临时存储在“某个地方”,将工作区还原当前版本未操作前的状态,相当于可以把当前工作现场“储藏”起来;
stash还可以将临时存储在“某个地方”的文件再次拿回到工作区,相当于恢复现场,可以继续开始后续工作

有时候我们要切换分支会有以下提示,是因为当前分支还有内容未提交,现在切换内容会丢失。这时候要用到git stash 命令
错误提示:
linhaifeng01@appledeMacBook-Pro test % git checkout tom
error: Your local changes to the following files would be overwritten by checkout:
    1.txt
Please commit your changes or stash them before you switch branches.
Aborting

在当前分支执行:
git stash # 会发现当前分支下所有文件都恢复到修改之前的状态,改动的状态都被存到stash区

然后后就可以切换到其他分支进行操作了
操作完毕后重新切到该分支下,执行git stash pop 即可还原之前的工作现场(将改动的内容都恢复到工作区)

详解如下:

file

#1、保存当前工作进度:git stash  
git stash 用来保存目前的工作目录和暂存区状态,并返回到干净的工作空间(即将工作区和暂存区恢复到修改之前)

#2、作用同上,message为此次进度保存的说明:git stash save message

#3、查看收藏的记录:git stash list
编号越小代表保存进度的时间越近。

#5、 将保存的内容重新恢复到工作目录:git stash apply stash@{num} 
恢复工作进度到工作区且该工作进度可重复恢复,此命令的stash@{num}是可选项,在多个工作进度中可以选择恢复,不带此项则默认恢复最近的一次进度相当于git stash apply stash@{0}

#6、将对应的stash记录删除:git stash drop stash@{num}
删除一条保存的工作进度,此命令的stash@{num}是可选项,在多个工作进度中可以选择删除,不带此项则默认删除最近的一次进度相当于git stash drop stash@{0}

#4、该命令=命令5+命令6:git stash pop stash@{num}  
恢复工作进度到工作区,此命令的stash@{num}是可选项,在多个工作进度中可以选择恢复,不带此项则默认恢复最近的一次进度相当于git stash pop stash@{0}

#7、命令:git stash clear
删除所有保存的工作进度。

示例

1、在dev分支正在进行开发任务,开发到一半,遇到了要在两小时修复代号487的bug,先保存工作现场,将工作区和暂存区还原到修改之前

$ git checkout dev
Already on 'dev'
$ ls
a.txt   b.txt   c.txt   d.txt

$ echo 111 >> a.txt  # 往文件a.txt添加内容
$ echo "eee" > e.txt  # 新增新文件
$ git add e.txt       # e.txt属于首次新增,需要add

$ git stash
Saved working directory and index state WIP on dev: d8dba1f v4

2、切换到出现bug的分支下(假设此时为master分支),创建子分支,在子分支上修改bug

$ git checkout master
$ git checkout -b issue-487

$ vim b.txt  # 修复bug,加上添加了一样内容fixed bug

# 修复完成后提交一个版本
$ git add b.txt 
$ git commit -m "fix bug 487"
[master 354e562] fix bug 487

3、修复完成后,切换到master分支,并完成合并,最后删除issue-487分支:

git checkout master
git merge --no-ff -m "merged bug fix 487" issue-487
git branch -d issue-487

4、重新切回dev分支,把stash的内容拿回来,继续干活

git checkout dev
git status
git stash list  # 可以看到有当初暂存的工作现场
git stash pop
git stash list # 无内容

#ps
你可以多次stash,恢复的时候,先用git stash list查看,然后恢复指定的stash,用命令:
git stash apply stash@{0}

思考:

dev分支当初就是从master分支分出来的,所以dev与master的源头一样,如果说master上的某个节点有bug,并且该节点处于dev分支之前,那么dev分支上肯定也有该bug,那如何修复呢?重复上述操作肯定可以,但重复操作毕竟不是那么聪明,我们其实可以这么做

同样的bug如果也想在dev上修复,而不是把整个master分支merge到当前的dev分支
git提供了一个cherry-pick命令,只把单独那个bug提交所做的修改复制到当前dev分支

$ git branch
  dev
* master

$ git log --graph --pretty=oneline --abbrev-commit
*   1331818 (HEAD -> master) merged bug fix 487
|\  
| * 50be59d fix bug 487 # 修复bug的提交
|/  
*   cb8e739 conflict fixed
|\  
| * d8dba1f (dev) v4
* | 5e6a978 v6
|/  
* 89e82b8 v3
* 68d496d v2
* 04352c4 v1
* 1452eed v0

$ git checkout dev
Switched to branch 'dev'

$ git cherry-pick 50be59d
[dev aee0ea6] fix bug 487
 Date: Wed Jul 15 20:41:47 2020 +0800
 1 file changed, 1 insertion(+)

$ cat b.txt # b.txt
bbb
fixed bug

git cherry-pick,我们就不需要在dev分支上手动再把修bug的过程重复一遍。

有些聪明的童鞋会想了,既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过你仍然需要git stash命令保存现场,才能从dev分支切换到master分支。

六 Feature分支

master分支应该是非常稳定的,也就是仅用来发布新版本,为了防止搞乱,平时不能在上面干活,我们通常在dev分支上开发代码,而开发的过程中肯定涉及到五花八门各种各样的功能要添加到软件中,为了不让新功能的代码搞乱dev,我们同样应该在dev上开辟子分支,称之为feature分支,在feature上开发完毕后,再切回dev分支进行合并,然后删除掉featrue分支即可

现在,你终于接到了一个新任务:开发代号为绿帽子的新功能,该功能计划用于为全世界的男人戴上绿帽子。

于是你准备开始

$ git branch
* dev
  master

$ git checkout -b 'feauture-greenhat'
Switched to a new branch 'feauture-greenhat'

$ touch greenhat.py  # 开发完毕
$ git add greenhat.py 
$ git commit -m "add feature greenhat"

切回dev分支,准备合并

$ git checkout dev
Switched to branch 'dev'

一切顺利的话,feature分支和bug分支都是类似的处理方式,合并,然后删除即可。

但是!

就在此时,接到上级命令,因经费不足,新功能必须取消!

虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:

$ git branch -d 'feauture-greenhat'
error: The branch 'feauture-greenhat' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feauture-greenhat'.
销毁失败。Git友情提醒,feature-vulcan分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D参数。。

# 现在我们强行删除:
$ git branch -D feauture-greenhat
Deleted branch feature-vulcan (was 287773e).
上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术