什么是git分支?
什么是git分支?首先让我们回顾一下提交对象,一个提交对象(commit objects)包括:
- 一系列文件在某个时间的快照。
- 一系列指向父提交对象的索引。
- 一个SHA-1名字,这个名字40个字符长,是独一无二的。
- 作者的姓名和邮箱,以及提交时对提交的描述。
事实上,“一系列文件在某个时间的快照”并不是直接存在于提交对象。在git中,blob对象保存着文件的快照,树对象保存着目录结构和blob对象的索引,而提交对象保存指向树对象的指针。下图是一个这三者关系的示意图:
那么git中的分支是什么呢?
git中的分支就像是你的文件的一份副本,你可以在需要的时候拷贝一份出来,这样你就得到了一个“分支”,你可以在上面修改,修改完成之后再合并回去。在一些版本控制软件中实际情况确实是这样,然而在git中并非如此。
在git中,对分支的操作大部分只是在修改指向提交对象的heads。我们知道,heads是一个指向提交对象的指针,分支操作中的大部分操作只需要修改heads的指向,即向heads文件中写入41个字符即可(40个SHA-1字符串和1个换行符)。与其他一些版本控制软件采用的复制文件策略相比较,git分支操作与文件大小无关,操作迅速快捷。
创建分支
现在先来看看我们在哪个分支,使用git branch
命令查看当前分支,命令选项-v
显示分支指向提交对象的校验和及其描述:
$ git branch
* master
$ git branch -v
* master 57b75e6 Add GitHub description.
从结果中看到,现在只有一个分支,叫做master
。*
表示当前所在的分支,即HEAD的指向。
用图简略表示如下:
现在创建一条dev
分支,使用git branch <branchname>
命令:
$ git branch dev
$ git branch
dev
* master
现在有了两条分支:master
和dev
,目前我们在master
分支。图示如下:
可见,事实上只是创建了一个指向图中提交对象C3
的指针,使用git log --decorate
可以查看heads的指向:
$ git log --oneline --decorate -3
57b75e6 (HEAD -> master, origin/master, dev) Add GitHub description.
beac1f4 make README.md more friendly.
14bd627 add two wrong line to README.md
master
、远程origin
的master
和dev
指向57b75e6
提交对象,HEAD
指向master
。
切换分支
现在切换到dev
分支,使用git checkout <branchname>
命令,在切换前请确保你的工作目录是干净的:
$ git checkout dev
Switched to branch 'dev'
这样就切换到了dev
分支,查看一下:
$ git branch
* dev
master
$ git log --oneline --decorate -3
57b75e6 (HEAD -> dev, origin/master, master) Add GitHub description.
beac1f4 make README.md more friendly.
14bd627 add two wrong line to README.md
可以看到我们确实在dev
分支,HEAD
确实指向了dev
分支。在切换分支时,git会将分支所指向的提交对象的文件快照检出到工作目录,并且更改HEAD
的指向。目前分支情况图示如下:
git checkout -b <branchname>
可以创建<branchname>
分支并且切换到它,相当于执行下面两条命令:
$ git branch <branchname>
$ git checkout <branchname>
“快进”合并
现在在dev
分支,我们创建一个dev.md
文件并且提交:
$ touch dev.md
$ git add dev.md
$ git commit -m "add dev.md"
[dev fd2e1cb] add dev.md
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev.md
查看一下各个分支所指:
$ git log --oneline --decorate -3
fd2e1cb (HEAD -> dev) add dev.md
57b75e6 (origin/master, master) Add GitHub description.
beac1f4 make README.md more friendly.
dev
前进了一个提交对象,HEAD
指向dev
,其他分支并没有更改,图式如下:
现在切换到master
,使用$ git checkout master
命令,HEAD
会指向master
,工作目录中的文件将会被替换:
合并分支使用git merge <branchname>
命令,这个命令将<branchname>
分支合并到当前分支,现在我们在master
分支,执行下面的命令将dev
分支合并到master
分支:
$ git merge dev
Updating 57b75e6..fd2e1cb
Fast-forward
dev.md | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev.md
git告诉我们此次合并的方式是Fast-forward
(快进),此时分支情况如下:
现在dev
分支已经被合并到master
分支了。从上图可以看出git仅仅是简单的更新了master
和HEAD
的指向,这是由于合并前master
指向dev
的直接上游,这种合并方式叫做快进(Fast-forward)。
可以使用--no-ff
选项避免使用“快进”合并,这样会形成一个新的合并提交,类似下节讲到的分之合并:
$ git merge --no-ff dev
现在dev
分支已经充分得发挥了自己的作用,让我们删除它:
$ git branch -d dev
Deleted branch dev (was fd2e1cb).
如果一个分支没有完全合并到当前分支,那么git会阻止你删除它,如果确实要删除它,使用-D
命令选项:
$ git branch -D <branchname>
如果想要知道那些分支被合并了或者没有合并,使用下面的命令:
$ git branch --merged # 查看已经被合并的分支
$ git branch --no-merged # 查看还没有被合并的分支
目前分支情况如下:
本文所讲的例子整体过程图示如下:
分支合并
现在创建一个testing
分支并且切换到该分支:
$ git checkout -b testing
Switched to a new branch 'testing'
添加testing.md
并提交,修改tesing.md
并提交:
$ touch testing.md
$ git add testing.md
$ git commit -m "add testing.md"
[testing dd4555e] add testing.md
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 testing.md
$ echo "A file added in testing branch." > testing.md
$ git commit -a -m "add description of testing.md"
[testing 40a00ae] add description of testing.md
1 file changed, 1 insertion(+)
回到master
分支并且修改dev.md
:
$ git checkout master
Switched to branch 'master'
$ echo "A dev file." > dev.md
$ ls
dev.md README.md
$ git commit -a -m "add description of dev.md"
[master 1b63c87] add description of dev.md
1 file changed, 1 insertion(+)
现在两条分支在分叉后都有新的提交:testing
有两个新的提交,master
有一个新的提交。怎样在命令行查看呢?
$ git log --oneline --decorate --graph --all
* 1b63c87 (HEAD -> master) add description of dev.md
| * 40a00ae (testing) add description of testing.md
| * dd4555e add testing.md
|/
* fd2e1cb add dev.md
* 57b75e6 (origin/master) Add GitHub description.
# 省略
可以看到,在fd2e1cb
分支分叉,testing
之后进行了两次提交,master
进行了一次提交,目前我们在master
分支。图示如下:
现在将testing
分支合并到master
分支:
$ git merge testing
Merge made by the 'recursive' strategy.
testing.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 testing.md
$ git log --oneline --decorate --graph --all
* 8425ef2 (HEAD -> master) Merge branch 'testing'
|\
| * 40a00ae (testing) add description of testing.md
| * dd4555e add testing.md
* | 1b63c87 add description of dev.md
|/
* fd2e1cb add dev.md
* 57b75e6 (origin/master) Add GitHub description.
# 省略
现在git帮我们合并了master
和testing
,并且生成了一个新的提交(你可能需要填写提交描述),这个新提交的SHA-1校验和前七位是8425ef2。
git能够帮我们自动合并,而不会产生冲突的原因是我们在不同的分支中修改了不同的文件,此时git会参考两个分支所指的快照(testing
的40a00ae
和master
的1b63c87
)和两个分支的共同祖先(fd2e1cb
),自动合并。参考的三个快照分别相当于下图的C6、C7和C4.
新生成的提交叫做合并提交,相当于下图的C8.这个新提交拥有两个父提交。
好了,现在删掉testing
分支吧:
$ git branch -d testing
Deleted branch testing (was 40a00ae).
本文所讲的分支合并的整体过程图示如下:
冲突解决
如果在不同分支中同一个文件的同一个地方做了修改,git就无法干净利落地合并它们。
创建一个新的分支iss1
,在iss1
分支中将README.md
修改如下并且提交:
$ git checkout -b iss1
Switched to a new branch 'iss1'
$ vim README.md
$ cat README.md
# Hi, Git!
This is my first git project and i use it to learn git.
Git is a free and open source distributed version control system.
$ git commit -a -m "change README.md in iss1"
[iss1 d6801d6] change README.md in iss1
1 file changed, 6 deletions(-)
切换到master
分支,将README.md
修改如下并且提交:
$ git checkout master
Switched to branch 'master'
$ vim README.md
$ cat README.md
# Hi, Git!
This is my first git project and i use it to learn git.
I LOVE GIT.
$ git commit -a -m "change README.md in master."
[master 63172f9] change README.md in master.
1 file changed, 1 insertion(+), 7 deletions(-)
$ git log --oneline --decorate --graph --all
* 63172f9 (HEAD -> master) change README.md in master.
| * d6801d6 (iss1) change README.md in iss1
|/
* 8425ef2 Merge branch 'testing'
# 省略
现在将iss1
分支合并到master
分支:
$ git merge iss1
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
git告诉我们说自动合并失败,原因是在README.md
文件中有冲突,并且提醒我们解决冲突后提交结果。
也就是说,git在遇到冲突时,并不会创建一个合并提交,而是暂停下来,等用户解决冲突之后,由用户提交。
含有冲突的文件被标记为“未合并”(unmerged)状态,随时可以使用git status
来查看:
$ 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: README.md
no changes added to commit (use "git add" and/or "git commit -a")
现在让我们解决README.md
中的冲突,首先来看一看git刚刚所做的工作:
$ cat README.md
# Hi, Git!
This is my first git project and i use it to learn git.
<<<<<<< HEAD
I LOVE GIT.
=======
Git is a free and open source distributed version control system.
>>>>>>> iss1
其中的一部分是git为我们标记的冲突的部分:
<<<<<<< HEAD
I LOVE GIT.
=======
Git is a free and open source distributed version control system.
>>>>>>> iss1
在=======
的上半部分的是HEAD
分支中的文件内容,在其下半部分的是iss1
分支中文件的内容。
现在让我们将这部分修改如下:
I LOVE GIT.
这表示将丢弃iss1
中的修改,当然你可以根据自己的喜好更改,你可以改成任意你需要的内容。
现在将文件添加到暂存区,并且查看状态:
$ git add README.md
$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
nothing to commit, working directory clean
可见,一旦冲突文件被添加到暂存区,它的“未合并”状态就会被解除,即表示冲突已经解决。
现在提交即可:
$ git commit -m "merge iss1"
[master 11f0f7a] merge iss1
$ git log --oneline --decorate --graph --all
* 11f0f7a (HEAD -> master) merge iss1
|\
| * d6801d6 (iss1) change README.md in iss1
* | 63172f9 change README.md in master.
|/
* 8425ef2 Merge branch 'testing'
# 省略
最后删除iss1
分支:
$ git branch -d iss1
Deleted branch iss1 (was d6801d6).
储藏与清理
git在切换分支时必须保证当前工作目录是干净的,如果现在做了一点更改,不至于提交一次新的更新,但是却必须更换到另一条分支上,怎么办呢?
git为我们提供了stash
(储藏)工具。
现在在master
分支上对README.md
作一些更改,并且将它储藏起来:
$ git status -s
M README.md
$ git stash
Saved working directory and index state WIP on master: 11f0f7a merge iss1
HEAD is now at 11f0f7a merge iss1
$ git status -s
$
在运行git stash
之后工作目录就变干净了,现在就可以切换到其他分支工作啦。
在其他分支工作完之后,又回到master
,怎样继续工作呢?
使用git stash list
命令可以查看储藏的列表:
$ git stash list
stash@{0}: WIP on master: 11f0f7a merge iss1
使用git stash apply <stashname>
即可应用,如果<stashname>
为空,则会应用最新的储藏:
$ git stash apply
$ git stash list
stash@{0}: WIP on master: 11f0f7a merge iss1
$ git stash apply
On branch master
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.md
no changes added to commit (use "git add" and/or "git commit -a")
$ git status -s
M README.md
我们的更改又回来了,使用git stash drop <stashname>
删除相应的储藏,如果<stashname>
为空,则会删除最新的储藏:
$ git stash drop
Dropped refs/stash@{0} (939ab1d7c4f88fe2dd9b3420d0cf919a668eff23)
$ git stash list
$
可以使用git stash pop
直接应用最新的储藏,同时删除该储藏。
在git中,可以进行多次储藏,也可以在不同的分支应用储藏。