-------------------------下面是阮一峰博士的git branch 逻辑结构图示----------------------------------------------
如果你严肃对待编程,就必定会使用”版本管理系统”(Version Control System)。
眼下最流行的”版本管理系统”,非Git莫属。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy1.png)
相比同类软件,git有很多优点。其中很显著的一点,就是版本的分支(branch)和合并(merge)十分方便。有些传统的版本管理软件,分支操作实际上会生成一份现有代码的物理拷贝,而Git只生成一个指向当前版本(又称”快照”)的指针,因此非常快捷易用。
但是,太方便了也会产生副作用。如果你不加注意,很可能会留下一个枝节蔓生、四处开放的版本库,到处都是分支,完全看不出主干发展的脉络。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy2.png)
Vincent Driessen提出了一个分支管理的策略(中文简译版),我觉得非常值得借鉴。它可以使得版本库的演进保持简洁,主干清晰,各个分支各司其职、井井有条。理论上,这些策略对所有的版本管理系统都适用,Git只是用来举例而已。如果你不熟悉Git,跳过举例部分就可以了。
一、主分支Master
首先,代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy3.png)
Git主分支的名字,默认叫做Master。它是自动建立的,版本库初始化以后,默认就是在主分支在进行开发。
二、开发分支Develop
主分支只用来分布重大版本,日常开发应该在另一条分支上完成。我们把开发用的分支,叫做Develop。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy4.png)
这个分支可以用来生成代码的最新隔夜版本(nightly)。如果想正式对外发布,就在Master分支上,对Develop分支进行”合并”(merge)。
Git创建Develop分支的命令:
git checkout -b develop master
将Develop分支发布到Master分支的命令:
# 切换到Master分支
git checkout master
# 对Develop分支进行合并
git merge –no–ff develop
这里稍微解释一下,上一条命令的–no–ff参数是什么意思。默认情况下,Git执行”快进式合并”(fast-farward merge),会直接将Master分支指向Develop分支。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy5.png)
使用–no–ff参数后,会执行正常合并,在Master分支上生成一个新节点。为了保证版本演进的清晰,我们希望采用这种做法。关于合并的更多解释,请参考Benjamin Sandofsky的《Understanding the Git Workflow》。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy6.png)
三、临时性分支
前面讲到版本库的两条主要分支:Master和Develop。前者用于正式发布,后者用于日常开发。其实,常设分支只需要这两条就够了,不需要其他了。
但是,除了常设分支以外,还有一些临时性分支,用于应对一些特定目的的版本开发。临时性分支主要有三种:
* 功能(feature)分支
* 预发布(release)分支
* 修补bug(fixbug)分支
这三种分支都属于临时性需要,使用完以后,应该删除,使得代码库的常设分支始终只有Master和Develop。
四、 功能分支
接下来,一个个来看这三种”临时性分支”。
第一种是功能分支,它是为了开发某种特定功能,从Develop分支上面分出来的。开发完成后,要再并入Develop。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy7.png)
功能分支的名字,可以采用feature-*的形式命名。
创建一个功能分支:
git checkout -b feature-x develop
开发完成后,将功能分支合并到develop分支:
git checkout develop
git merge –no-ff feature-x
删除feature分支:
git branch -d feature-x
五、预发布分支
第二种是预发布分支,它是指发布正式版本之前(即合并到Master分支之前),我们可能需要有一个预发布的版本进行测试。
预发布分支是从Develop分支上面分出来的,预发布结束以后,必须合并进Develop和Master分支。它的命名,可以采用release-*的形式。
创建一个预发布分支:
git checkout -b release-1.2 develop
确认没有问题后,合并到master分支:
git checkout master
git merge –no-ff release-1.2
# 对合并生成的新节点,做一个标签
git tag -a 1.2
再合并到develop分支:
git checkout develop
git merge –no-ff release-1.2
最后,删除预发布分支:
git branch -d release-1.2
六、修补bug分支
最后一种是修补bug分支。软件正式发布以后,难免会出现bug。这时就需要创建一个分支,进行bug修补。
修补bug分支是从Master分支上面分出来的。修补结束以后,再合并进Master和Develop分支。它的命名,可以采用fixbug-*的形式。
![阮一峰:Git分支管理策略 阮一峰:Git分支管理策略](http://jbcdn2.b0.upaiyun.com/2012/07/Git-branch-management-strategy8.png)
创建一个修补bug分支:
git checkout -b fixbug-0.1 master
修补结束后,合并到master分支:
git checkout master
git merge –no-ff fixbug-0.1
git tag -a 0.1.1
再合并到develop分支:
git checkout develop
git merge –no-ff fixbug-0.1
最后,删除”修补bug分支”:
git branch -d fixbug-0.1
-------------------------下面是小鱼同志的git branch 带hash结构图示---------------------------------------------
熟悉git分支的原理是掌握了git的精髓,因为git和我们常用的源码管理系统有很大的区别和优点在分支上可以体现出来,一般我们常用的源码管理系统分支都是需要创建新目录,有全新的源码copy,一般都需要创建一个源代码目录完整的副本。对应大项目来说非常的耗费时间和空间。git正式因为其优秀的分支模式可以从源码管理系统中脱颖而出。因为git的分支非常的轻量级,他的操作机会瞬间完成,在不同的分支切换也非常快速。与其他版本相比,git更加推崇使用分支管理。分支是一个git非常强大和高效的工具。熟悉使用可以大大的提高工作效率和开发效率。
1、分支
git分支是如果存储数据,我们通过git数据存储的方式可以知道,存储一些列文件快照的方式。而一般常用的源码管理软件保存的基于文件版本差异的变化保存数据。
在git提交中,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包括本次提交的作业的相关附属信息,包含零个或多个指向该提交对象的父对象指针,首次提交时没有直接的祖先,普通提交有一个祖先,由两个或多个分支合并产生的提交有多个祖先。
假如我们的工作目录有三个文件,准备将他们暂存后提交,暂存操作会对每一个文件计算校验和(Sha1哈希字符串)。然后把当前版本文件快照保存到git仓库中。git使用blob类型对象存储这些快照。并将校验和加入暂存区。
当使用git commit 新建一个提交对象前,git会计算每一个子目录或者项目的根目录的校验和,然后在git仓库中将这些目录保存为(tree)树对象。之后git创建的提交对象,除了包含相关提交信息之外,还包含指向这个树对象(项目根目录)的指针。如此他就可以在将来需要的时候,重现此次快照的内容。
$ git add test1.txt test2.txt test3.txt
$ git commit -m " this is the firt commit"
[master (root-commit) 680e70a] this is the firt commit
3 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 test1.txt
create mode 100644 test2.txt
create mode 100644 test3.txt
$ find .git/objects/ -type f
.git/objects/15/f2ed7f3d000ee95bf0e343f40eb892f622174d
.git/objects/30/758feb48e8bdba6ace1ecadf7bf2f89b94c115
.git/objects/5b/3c0c1c42386a00a8f81854b84628305f7082c7
.git/objects/68/0e70ac4e4376992c7d90905b8351badd233850
.git/objects/97/22bcfd0dcf84bbc82ce6095055010c011f91e6
以上是我刚刚创建的一个空的仓库,添加test1.txt test2.txt test3.txt 三个文件。添加到暂存区后,在。git/objects/ 对象中会新增加三个文件快照blog 对象。暂存.git/index 会指向对应的对象。首次提交到仓库后,仓库中在.git/objecs/目录中有5个对象。比在暂存区中增加了两个对象,是因为一个是根目录树(tree)对象。和一个提交commit 的对象。根目录tree对象指向三个文件blob的索引对象和保存文件内容的名称。而commit 对象会指向树对象,同时保存对象说明及相关的附属信息。仓库对象之间的关系图可以如下图。
当连续多次提及后,每次创建一个提交commit 对象会指向上一个提交的父对象。已经提交了三个对象,可以参照如下图:
$ git log --oneline
4d38d40 the third commit
66a1a2e the secomd commit
680e70a this is the firt commit
2、 创建分支
git bran branchname 这个命令会在当前活动分支的基础上创建一个新的分支。其实是在当前活动分支对象commit 新添加一个指向该对象的分支指针,如我们在现master 分支的基础上创建一个testing 分支。git branch testing 创建分支如下关系图:
git 当前活动分支,即工作分支是是哪个呢,可以通过命令git branch 查看,*号标示的就是当前活动分支。有一个head 特别指针保存着当前活动分支。具体物理地址是保存在.git/head 文件中,他指向的是一个分支名称,分支明确refs/heads/master 指向的是当前分支指向的commit 对象。他是指向当前工作本地活动分支的指针。看如下命令,我么可以通过git checkout branchname 转换分支。如git checkout testing 后就指向了testing 分支。看如下图转变过程。
$ git branch
* master
testing
$ ls .git/refs/heads
master testing
$ cat .git/refs/heads/master
4d38d402b3873d95c03cb3e1d2c65bf9aa3b5894
$ cat .git/refs/heads/testing
4d38d402b3873d95c03cb3e1d2c65bf9aa3b5894
$ cat .git/head
ref: refs/heads/master
$ git checkout testing
Switched to branch 'testing'
$ cat .git/head
ref: refs/heads/testing
当前活动分支是在testing 分支,我们在testing 分支做修改后提交。这是testing 分支指向最近一个提交。master 分支是指向当前提交的父提交commit 对象。
现在我们切换到master 分支,并把工作目录文件换成master分支指向的快照内容,现在所做的修改时在老的版本上做修改,在master 分支上做修改。修改不同余testing 分支另一条分支提交。如下图结果。
$ git commit -a -m "testing branch change"
[testing 0c8f2de] testing branch change
1 files changed, 1 insertions(+), 0 deletions(-)
$ git checkout master
Switched to branch 'master'
$ git commit -a -m "the master branch commit "
[master 1d37642] the master branch commit
1 files changed, 2 insertions(+), 0 deletions(-)
如现在我们需要把testing 分支和 master 分支合并。合并操作命令git merge branch name ,查看分支日志。
$ git merge testing
Merge made by the 'recursive' strategy.
test2.txt | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
$ git log --oneline --graph
* ac631f6 Merge branch 'testing'
|\
| * 0c8f2de testing branch change
* | 1d37642 the master branch commit
|/
* 4d38d40 the third commit
* 66a1a2e the secomd commit
* 680e70a this is the firt commit
由于分支实际上仅仅是一个包含所指对象校验和(40个字符串长度的sha1字符串)文件。所以创建和销毁一个分支会非常的容易和廉价。实际上新建一个分支就是向一个文件写入41个字节(一个换行符)。指向一某个提交点。
正式由于这个特性和其它版本形成了特别鲜明的对比。原始的源码管理系统管理分支一般都采用备份所有项目文件到特定的目录,所以根据项目文件数量和大小不同,消耗的时间随着项目的大小时间也不同。而gti 无论项目大小不同,他的特性与项目的复杂度无关,永远可以在几毫秒内创建分支和切换分支。同时每次回记录和指向父对象。这位将来要合并对象对参照标准基础。
3、分支合并创建实例模型
分支创建和合并的例子,实际工作中工作流程
1、开发一个项目
2、为显示某个新的需求,创建一个新的分支。
3、在这个分支的开发
4、如果此时项目有bug需要立即修复需要在原先发布到生产环境的版本的分支。
5、为这次紧急bugfix 创建一个新的分支,并在其中修复问题。
6、bug修复没问题后,会到开发服务器生产分支,和修补分支合并,然后推送到生产服务器上。
7、所有一切都完成后,切换到之前新需求分支,继续工作。
接着刚刚写的范例,我们在主分支发布到生产环境后,已经推送到中心服务器,当前的状态如下图:
1、现在有新的需求需要开发,所以在当地master 分支上创建一个新的开发分支。然后再开发分支上做开发。用到的命令式git checkout -b dev ,相当于git branch dev ,git checkout dev 两条命令。当前工作活动分支是指向dev开发分支,我们在新的开发分支上做修改提交后。如下图:
$ git checkout -b dev
Switched to a new branch 'dev'
$ git commit -a -m "dev branch revise"
[dev 46324fb] dev branch revise
1 files changed, 1 insertions(+), 0 deletions(-)
现在正式的生产环境发现有bug 需要修复,现在我们不需要在创建和发布到修复服务器上花大力气做修改,现在需要做的只需要切回到master分支,然后再master 的分支上创建bugfix分支。在分支切回过程有一点需要注意的是。在暂存区和工作目录是否还没有提交的修改,如果他和你即将检出的分支产生冲突从而阻止切回分支。在切换分支前需要保持一个干净的工作区域。
$ git checkout master
Switched to branch 'master'
$ git checkout -b bugfix
Switched to a new branch 'bugfix'
$ git commit -a -m "bug fix branch commit"
[bugfix 7599941] bug fix branch commit
1 files changed, 2 insertions(+), 0 deletions(-)
在bugfix 分支上测试成功后,发现已经ok了,需要把master分支合并进来,然后推送到中心仓库服务器。先切换成到master 分支,然后git merge 合并bugfix 分支做合并,你会发现fast-forfward 合并,git只需要把master分支指针直接右移,如果顺着一个分支走下去可以到达另一个分支,那么在git 合并的时候,只会简单的把指针右移,因为这种单线历史分支不存在解决冲突和分歧。所以这种合并过程成为快进(fast forward). 当master 分支和bugfix 分支合并后,见下图,当前的bugfix 分支和master 分支会指向一个相同的提交对象。如果bugfix 分支已经没有存在的必要了,可以使用git branch -d branch 进行删除。
$ git merge bugfix
Updating ac631f6..7599941
Fast-forward
test1.txt | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
$ git log --graph --oneline
* 7599941 bug fix branch commit
* ac631f6 Merge branch 'testing'
|\
| * 0c8f2de testing branch change
* | 1d37642 the master branch commit
|/
* 4d38d40 the third commit
* 66a1a2e the secomd commit
* 680e70a this is the firt commit
$ git branch -d bugfix
Deleted branch bugfix (was 7599941).
以上bug修复完成后,可以切换到dev 分支,继续需求开发,可以将master 分支合并到dev 分支,或者等dev 版本开发完成后,再将dev分支更新到并入master.现在分支不同于hotfix 合并的方式,因为这次从历史ac631 处有分支同父对象,dev 和master 分支在进行合并的时候,会找到相同的祖先分支。ac631 和最新分支末端 46324fb 和 22ef87 进行一个三方合并运算,合并提交三个对象,git没有简单的把分支指正右移,而是三方合并结果做一个新的快照创建一个提交commit 对象1421fd5 。这个提交对象有两个祖先,有一点需要注意的是,git 可以决定哪个祖先是最佳合并的基础,这和CVS 和 SVN 不同,他们需要开发者手工合并基础,所以此特性合并操作会让git操作会比其它系统要简单许多。
$ git merge dev
Merge made by the 'recursive' strategy.
$ git log --oneline --graph
* 1421fd5 Merge branch 'dev'
|\
| * 46324fb dev branch revise
* | 22ef787 before dev merege commit
* | 7599941 bug fix branch commit
|/
* ac631f6 Merge branch 'testing'
|\
| * 0c8f2de testing branch change
* | 1d37642 the master branch commit
|/
* 4d38d40 the third commit