Git3:Git分支 一、概念 二、创建与合并分支 三、解决冲突 四、分支管理策略 五、 bug分支 六、推送和拉取远程分支

分支就像漫威漫画宇宙里的平行宇宙。在一个宇宙中,美国队长是正义的化身,是复仇者的领导者。而在另一个宇宙中,美队成了九头蛇。

两个平行宇宙互不干扰,那么也没啥影响。不过在某个时间点,两个宇宙交叉了,于是就出现了死侍大战死侍。

而每一个平行宇宙就相当于一个分支。平行宇宙会在某个时间点出现交叉,而分支也会在某个时间点被合并。

一个团队中有多个人在开发一下项目,某个同事在开发一个新的功能,需要一周时间完成,他写了其中的30%还没有写完,如果他提交了这个版本,那么团队中的其他人就不能继续开发了。但是等到他全部写完再全部提交,大家又看不到他的开发进度,也不能继续干活,这如何是好呢?

对于上面的这个问题,我们就可以用分支管理的办法来解决,一同事开发新功能他可以创建一个属于他自己的分支,其它同事暂时看不到,继续在开发分支(一般都有多个分支)上干活,他在自己的分支上干活,等他全部开发完成,再一次性的合并到开发分支上,这样我们既可知道他的开发进度,又不影响大家干活。

分支本质上其实就是一个指向某次提交的可变指针。Git 的默认分支名字为 master 。而我们是怎么知道当前处于哪个分支当中呢?答案就是在于 HEAD 这个十分特殊的指针,它专门用于指向于本地分支中的当前分支

二、创建与合并分支

1、创建分支原理分析

之前我们每次的版本提交,Git都把它们串成一条时间线,这条时间线就是一个分支。默认情况下,Git只有一个分支,即主分支,也叫master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。

一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

每次提交,master分支都会向前移动一步,随着不断提交,master分支的线也越来越长。

当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!不过,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

假如我们在dev上的工作完成了,就可以把dev合并到master上。最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

2、创建分支语法

创建一个分支并切换到新分支上:

# 创建一个dev分支,并切换到dev分支上,-b参数的意思是创建并切换分支
git checkout -b dev 

# 相当于执行了如下两个命令:
git branch dev
git checkout dev

查看当前位于哪个分支:

git branch

查看当前仓库的所有分支:

git branch -a

切换分支:

#切换到master分支
git branch master    

合并分支:

git checkout master
git merge dev        #当dev分支合并到master分支上

删除分支:

git branch -d dev

#当一个分支还未被合并时,使用-d选项无法删除,需要使用-D选项强行删除
git branch -D dev   

三、解决冲突

创建一个新的dev分支:

yanwei@ubuntu:~/git_test$ git checkout -b dev
切换到一个新分支 'dev'
yanwei@ubuntu:~/git_test$ git branch
* dev
  master

修改code.txt内容并提交:

# 修改后的code.txt内容如下:
yanwei@ubuntu:~/git_test$ cat code.txt 
this is the first line
this is the second line
this is the third line
this is the forth line
this is dev branch

yanwei@ubuntu:~/git_test$ git add code.txt 
yanwei@ubuntu:~/git_test$ git commit -m "dev first commit"
[dev 187dee6] dev first commit
 1 file changed, 1 insertion(+)

再切换回master分支,修改code.txt内容并提交:

# 切回master分支
yanwei@ubuntu:~/git_test$ git checkout master
切换到分支 'master'

# 修改内容并提交:
yanwei@ubuntu:~/git_test$ cat code.txt 
this is the first line
this is the second line
this is the third line
this is the forth line
this is the master branch
yanwei@ubuntu:~/git_test$ git add code.txt
yanwei@ubuntu:~/git_test$ git commit -m "master new commit"
[master 2015000] master new commit
 1 file changed, 1 insertion(+)

现在master分支和dev分支就各自有了新的提交 ,变成了这样:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突:

yanwei@ubuntu:~/git_test$ git merge dev
自动合并 code.txt
冲突(内容):合并冲突于 code.txt
自动合并失败,修正冲突然后提交修正的结果。

git告诉我们code.txt这个文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件:

yanwei@ubuntu:~/git_test$ git status
位于分支 master
您有尚未合并的路径。
  (解决冲突并运行 "git commit")
  (使用 "git merge --abort" 终止合并)

未合并的路径:
  (使用 "git add <文件>..." 标记解决方案)

	双方修改:   code.txt

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")

我们查看code.txt的内容如下:

yanwei@ubuntu:~/git_test$ cat code.txt 
this is the first line
this is the second line
this is the third line
this is the forth line
<<<<<<< HEAD
this is the master branch
=======
this is dev branch
>>>>>>> dev

Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,我们修改如下后保存如下:

yanwei@ubuntu:~/git_test$ cat code.txt 
this is the first line
this is the second line
this is the third line
this is the forth line
this is the master branch
this is dev branch

再次提交:

yanwei@ubuntu:~/git_test$ git add code.txt
yanwei@ubuntu:~/git_test$ git commit -m "解决冲突"
[master 6c1828d] 解决冲突

这个时候,master分支和dev分支就变成了如下图所示:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

我们也可以通过如下指令查看到分支合并情况:

yanwei@ubuntu:~/git_test$ git log --graph --oneline 
*   6c1828d (HEAD -> master) 解决冲突
|  
| * 187dee6 (dev) dev first commit
* | 2015000 master new commit
|/  
* 0a96a0f forth commit
* e4fb2aa third commit
* 227ecaa second commit
* d66bdc0 first commit

最后删除dev分支:

yanwei@ubuntu:~/git_test$ git branch -d dev
已删除分支 dev(曾为 187dee6)。

四、分支管理策略

1、保留分支历史

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

下面通过一个例子来说明--no-ff方式的git merge:

再次创建并切换到dev分支

yanwei@ubuntu:~/git_test$ git checkout -b dev
切换到一个新分支 'dev'

修改code.txt内容并提交版本:

yanwei@ubuntu:~/git_test$ cat code.txt 
this is the first line
this is the second line
this is the third line
this is the forth line
this is the master branch
this is dev branch
this is dev branch new line

yanwei@ubuntu:~/git_test$ git add code.txt
yanwei@ubuntu:~/git_test$ git commit -m 'dev branch another commit'
[dev b89266d] dev branch another commit
 1 file changed, 1 insertion(+)

切换回master分支,然后通过--no-ff选项合并分支:

yanwei@ubuntu:~/git_test$ git checkout master
切换到分支 'master'
yanwei@ubuntu:~/git_test$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
 code.txt | 1 +
 1 file changed, 1 insertion(+)

最后查看分支历史:

yanwei@ubuntu:~/git_test$ git log --graph --oneline 
*   ea9a7d5 (HEAD -> master) merge with no-ff
|  
| * b89266d (dev) dev branch another commit
|/  
*   6c1828d 解决冲突
|  
| * 187dee6 dev first commit
* | 2015000 master new commit
|/  
* 0a96a0f forth commit
* e4fb2aa third commit
* 227ecaa second commit
* d66bdc0 first commit

可以看到,不使用Fast forward模式,merge后就像这样:

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

2、分支管理原则

在实际开发中,针对分支的管理,我们应该遵循以下几个基本原则:

  1. master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
  2. 干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
  3. 你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

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

Git3:Git分支
一、概念
二、创建与合并分支
三、解决冲突
四、分支管理策略
五、 bug分支
六、推送和拉取远程分支

五、 bug分支

软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,当前正在dev上进行的工作还没有提交:

yanwei@ubuntu:~/git_test$ git status
位于分支 master
尚未暂存以备提交的变更:
  (使用 "git add <文件>..." 更新要提交的内容)
  (使用 "git checkout -- <文件>..." 丢弃工作区的改动)

	修改:     code.txt

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")

并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?幸好,Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:

yanwei@ubuntu:~/git_test$ git stash
保存工作目录和索引状态 WIP on master: ea9a7d5 merge with no-ff

现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),这时候,就可以放心的创建bug分支来修复bug。

首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:

yanwei@ubuntu:~/git_test$ git checkout master
切换到分支 'master'
yanwei@ubuntu:~/git_test$ git checkout -b issue-101
切换到一个新分支 'issue-101'

然后在issue-101分支上进行bug修复并提交,最后合并到master,最后删除bug分支:

# 在bug分支创建一个readme.txt
yanwei@ubuntu:~/git_test$ touch readme.txt

#提交该文件
yanwei@ubuntu:~/git_test$ git add readme.txt 
yanwei@ubuntu:~/git_test$ git commit -m "修复bug"
[issue-101 67de5f6] 修复bug
 1 file changed, 1 insertion(+)
 create mode 100644 readme.txt

# 切换到master合并bug分支 
yanwei@ubuntu:~/git_test$ git checkout master
切换到分支 'master'
yanwei@ubuntu:~/git_test$ git merge --no-ff -m "合并bug分支" issue-101
Merge made by the 'recursive' strategy.
 readme.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 readme.txt

#删除bug分支
yanwei@ubuntu:~/git_test$ git branch -d issue-101
已删除分支 issue-101(曾为 67de5f6)。

bug修复完成以后,现在要回到dev分支干活:

yanwei@ubuntu:~/git_test$ git checkout dev
切换到分支 'dev'

工作区是干净的,刚才的工作现场存到哪去了?用git stash list命令看看:

yanwei@ubuntu:~/git_test$ git stash list
stash@{0}: WIP on dev: b89266d dev branch another commit

工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:

  1. git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
  2. git stash pop,恢复的同时把stash内容也删了
yanwei@ubuntu:~/git_test$ git stash pop
位于分支 dev
尚未暂存以备提交的变更:
  (使用 "git add <文件>..." 更新要提交的内容)
  (使用 "git checkout -- <文件>..." 丢弃工作区的改动)

	修改:     code.txt

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")
丢弃了 refs/stash@{0} (971af18ec9510e4175dc7e7f4e11417731040231)

还可以多次git stash,然后通过git stash list查看stash列表,如果要恢复指定的stash,可以使用如下方法:

# 查看stash列表
yanwei@ubuntu:~/git_test$ git stash list
stash@{0}: WIP on master: d69c612 合并bug分支

# 恢复指定的stash
yanwei@ubuntu:~/git_test$ git stash apply stash@{0}
位于分支 master
尚未暂存以备提交的变更:
  (使用 "git add <文件>..." 更新要提交的内容)
  (使用 "git checkout -- <文件>..." 丢弃工作区的改动)

	修改:     code.txt

未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)

	new.txt

修改尚未加入提交(使用 "git add" 和/或 "git commit -a")

六、推送和拉取远程分支

推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:

$ git push origin master

如果要推送其他分支,比如dev,就改成:

$ git push origin dev

但是,并不是一定要把本地分支往远程推送,需要推送的远程分支说明:

  • master分支是主分支,因此要时刻与远程同步;
  • dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug分支只用于在本地修复bug,就没必要推到远程
  • feature分支是否推到远程,取决于你是否和同事在上面协作开发。

多人协作时,大家都会往master和dev分支上推送各自的修改。现在,假如你的同事在另一台电脑上克隆远程仓库:

$ git clone git@github.com:michaelliao/learngit.git

当你的同事从远程库clone时,默认情况下,看到本地的master分支。现在,要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支:

$ git checkout -b dev origin/dev

现在,他就可以在dev上继续修改,然后,时不时地把dev分支push到远程:

$ git commit -m "add /usr/bin/env"

你的同事已经向origin/dev分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:

# 提交修改
$ git add hello.py 

$git commit -m "add coding: utf-8"
[dev bd6ae48] add coding: utf-81 file changed, 1 insertion(+)

# 推送远程dev分支
$ git push origin dev
To git@github.com:michaelliao/learngit.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to'git@github.com:michaelliao/learngit.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards'in'git push --help'for details.

推送失败,因为你的同事的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送:

$ git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From github.com:michaelliao/learngit
   fc38031..291bea8  dev        -> origin/dev
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream dev origin/<branch>

git pull也失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接:

$ git branch --set-upstream dev origin/dev
Branch dev set up to track remote branch dev from origin.

再pull:

$ git pull
Auto-merging hello.py
CONFLICT (content):Merge conflict in hello.py
Automatic merge failed; fix conflicts andthen commit the result.

这回git pull成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push:

$ git commit -m "merge & fix hello.py"
[dev adca45d] merge & fix hello.py
$ git push origin dev
Counting objects: 10, done.
Delta compression using up to4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 747 bytes, done.
Total 6 (delta 0), reused 0 (delta 0)
To git@github.com:michaelliao/learngit.git
   291bea8..adca45d  dev -> dev

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin branch-name推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用git push origin branch-name推送就能成功!
  5. 如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream branch-name origin/branch-name