有人把 Git 的分支模型称为它的`‘必杀技特性’',也正由于这一特性,使得 Git 从众多版本控制系统中脱颖而出。 为什么 Git 的分支模型如此出众呢? Git 处理分支的方式可谓是难以置信的轻量,建立新分支这一操做几乎能在瞬间完成,而且在不一样分支之间的切换操做也是同样便捷。 与许多其它版本控制系统不一样,Git 鼓励在工做流程中频繁地使用分支与合并,哪怕一天以内进行许屡次。 理解和精通这一特性,你便会意识到 Git 是如此的强大而又独特,而且今后真正改变你的开发方式。html
在进行提交操做时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,咱们能够很天然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不只仅是这样,该提交对象还包含了做者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操做产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象,git
为了更加形象地说明,咱们假设如今有一个工做目录,里面包含了三个将要被暂存和提交的文件。 暂存操做会为每个文件计算校验和(使用咱们在 起步 中提到的 SHA-1 哈希算法),而后会把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交:github
$ git add README test.rb LICENSE $ git commit -m 'The initial commit of my project'
当使用 git commit
进行提交操做时,Git 会先计算每个子目录(本例中只有项目根目录)的校验和,而后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会建立一个提交对象,它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就能够在须要的时候重现这次保存的快照。web
如今,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和全部提交信息)。算法
你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master
分支。HEAD
严格来讲不是指向提交,而是指向master
,master
才是指向提交的,因此,HEAD
指向的就是当前分支。shell
一开始的时候,master
分支是一条线,Git用master
指向最新的提交,再用HEAD
指向master
,就能肯定当前分支,以及当前分支的提交点:vim
Git 是怎么建立新分支的呢? 很简单,它只是为你建立了一个能够移动的新的指针。 好比,建立一个 testing 分支, 你须要使用 git branch
命令:bash
$ git branch testing
要切换到一个已存在的分支,你须要使用 git checkout
命令。 咱们如今切换到新建立的 testing
分支去:app
$ git checkout testing
那么,这样的实现方式会给咱们带来什么好处呢? 如今不妨再提交一次:工具
$ vim test.rb $ git commit -a -m 'made a change'
首先,咱们建立dev
分支,而后切换到dev
分支:
$ git checkout -b dev Switched to a new branch 'dev'
git checkout
命令加上-b
参数表示建立并切换,至关于如下两条命令:
$ git branch dev $ git checkout dev Switched to branch 'dev'
而后,用git branch
命令查看当前分支:
$ git branch * dev master
git branch
命令会列出全部分支,当前分支前面会标一个*
号。
而后,咱们就能够在dev
分支上正常提交,好比对readme.txt作个修改,加上一行:
Creating a new branch is quick.
你可使用带 -d
选项的 git branch
命令来删除分支:
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
假设你已经修正了 #53 问题,而且打算将你的工做合并入 master
分支。 为此,你须要合并 iss53
分支到 master
分支,这和以前你合并 hotfix
分支所作的工做差很少。 你只须要检出到你想合并入的分支,而后运行 git merge
命令:
$ git checkout master Switched to branch 'master' $ git merge iss53 Merge made by the 'recursive' strategy. index.html | 1 + 1 file changed, 1 insertion(+)
这和你以前合并 hotfix
分支的时候看起来有一点不同。 在这种状况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。 由于,master
分支所在提交并非 iss53
分支所在提交的直接祖先,Git 不得不作一些额外的工做。 出现这种状况的时候,Git 会使用两个分支的末端所指的快照(C4
和 C5
)以及这两个分支的工做祖先(C2
),作一个简单的三方合并。
有时候合并操做不会如此顺利。 若是你在两个不一样的分支中,对同一个文件的同一个部分进行了不一样的修改,Git 就无法干净的合并它们。 若是你对 #53 问题的修改和有关 hotfix
的修改都涉及到同一个文件的同一处,在合并它们的时候就会产生合并冲突:
$ git merge iss53 Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result.
此时 Git 作了合并,可是没有自动地建立一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你能够在合并冲突后的任意时刻使用 git status
命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:
$ git status On branch master You have unmerged paths. (fix conflicts and run "git commit") Unmerged paths: (use "git add <file>..." to mark resolution) both modified: index.html no changes added to commit (use "git add" and/or "git commit -a")
任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样你能够打开这些包含冲突的文件而后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:
<<<<<<< HEAD:index.html <div id="footer">contact : email.support@github.com</div> ======= <div id="footer"> please contact us at support@github.com </div> >>>>>>> iss53:index.html
这表示 HEAD
所指示的版本(也就是你的 master
分支所在的位置,由于你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(=======
的上半部分),而 iss53
分支所指示的版本在 =======
的下半部分。 为了解决冲突,你必须选择使用由 =======
分割的两部分中的一个,或者你也能够自行合并这些内容。 例如,你能够经过把这段内容换成下面的样子来解决冲突:
<div id="footer"> please contact us at email.support@github.com </div>
上述的冲突解决方案仅保留了其中一个分支的修改,而且 <<<<<<<
, =======
, 和 >>>>>>>
这些行被彻底删除了。 在你解决了全部文件里的冲突以后,对每一个文件使用 git add
命令来将其标记为冲突已解决。 一旦暂存这些本来有冲突的文件,Git 就会将它们标记为冲突已解决。
如今已经建立、合并、删除了一些分支,让咱们看看一些经常使用的分支管理工具。
git branch
命令不仅是能够建立与删除分支。 若是不加任何参数运行它,会获得当前全部分支的一个列表:
$ git branch iss53 * master testing
注意 master
分支前的 *
字符:它表明如今检出的那一个分支(也就是说,当前 HEAD
指针所指向的分支)。 这意味着若是在这时候提交,master
分支将会随着新的工做向前移动。 若是须要查看每个分支的最后一次提交.
软件开发中,bug就像屡见不鲜同样。有了bug就须要修复,在Git中,因为分支是如此的强大,因此,每一个bug均可以经过一个新的临时分支来修复,修复后,合并分支,而后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很天然地,你想建立一个分支issue-101
来修复它,可是,等等,当前正在dev
上进行的工做尚未提交:
$ git status On branch dev Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: hello.py Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: readme.txt
并非你不想提交,而是工做只进行到一半,还无法提交,预计完成还需1天时间。可是,必须在两个小时内修复该bug,怎么办?
幸亏,Git还提供了一个stash
功能,能够把当前工做现场“储藏”起来,等之后恢复现场后继续工做:
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge
如今,用git status
查看工做区,就是干净的(除非有没有被Git管理的文件),所以能够放心地建立分支来修复bug。
首先肯定要在哪一个分支上修复bug,假定须要在master
分支上修复,就从master
建立临时分支:
$ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 6 commits. (use "git push" to publish your local commits) $ git checkout -b issue-101 Switched to a new branch 'issue-101'
如今修复bug,须要把“Git is free software ...”改成“Git is a free software ...”,而后提交:
$ git add readme.txt $ git commit -m "fix bug 101" [issue-101 4c805e2] fix bug 101 1 file changed, 1 insertion(+), 1 deletion(-)
修复完成后,切换到master
分支,并完成合并,最后删除issue-101
分支:
$ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 6 commits. (use "git push" to publish your local commits) $ git merge --no-ff -m "merged bug fix 101" issue-101 Merge made by the 'recursive' strategy. readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
太棒了,原计划两个小时的bug修复只花了5分钟!如今,是时候接着回到dev
分支干活了!
$ git checkout dev Switched to branch 'dev' $ git status On branch dev nothing to commit, working tree clean
工做区是干净的,刚才的工做现场存到哪去了?用git stash list
命令看看:
$ git stash list stash@{0}: WIP on dev: f52c633 add merge
工做现场还在,Git把stash内容存在某个地方了,可是须要恢复一下,有两个办法:
一是用git stash apply
恢复,可是恢复后,stash内容并不删除,你须要用git stash drop
来删除;
另外一种方式是用git stash pop
,恢复的同时把stash内容也删了:
$ git stash pop On branch dev Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: hello.py Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: readme.txt Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)
再用git stash list
查看,就看不到任何stash内容了:
$ git stash list
你能够屡次stash,恢复的时候,先用git stash list
查看,而后恢复指定的stash,用命令:
$ git stash apply stash@{0}
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你确定不但愿由于一些实验性质的代码,把主分支搞乱了,因此,每添加一个新功能,最好新建一个dev分支,在上面开发,完成后,合并,最后,删除该dev分支。
如今,你终于接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。
因而准备开发:
$ git checkout -b dev-vulcan Switched to a new branch 'dev-vulcan'
5分钟后,开发完毕:
$ git add vulcan.py $ git status On branch dev-vulcan Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: vulcan.py $ git commit -m "add feature vulcan" [feature-vulcan 287773e] add feature vulcan 1 file changed, 2 insertions(+) create mode 100644 vulcan.py
切回dev
,准备合并:
$ git checkout dev
一切顺利的话,feature分支和bug分支是相似的,合并,而后删除。
可是!
就在此时,接到上级命令,因经费不足,新功能必须取消!
虽然白干了,可是这个包含机密资料的分支仍是必须就地销毁:
$ git branch -d dev-vulcan error: The branch 'dev-vulcan' is not fully merged. If you are sure you want to delete it, run 'git branch -D dev-vulcan'.
销毁失败。Git友情提醒,dev-vulcan
分支尚未被合并,若是删除,将丢失掉修改,若是要强行删除,须要使用大写的-D
参数。。
如今咱们强行删除:
$ git branch -D dev-vulcan
Deleted branch dev-vulcan (was 287773e).
终于删除成功!
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.