之前的学习过程里,所有的操作都是在主分支master里进行的。之前我们说过HEAD
是指向某个提交版本的,事实上这个说法并不完全正确。确切地说HEAD
是指向某个分支的,而分支才是指向版本的。每次提交,master
分支都会向前移动一步,这样,随着你不断提交,master
分支的线也越来越长。
创建和合并分支
如果创建新的分支,例如new
时,Git新建了一个指针叫new
,指向master
相同的提交,再把HEAD
指向dev
,就表示当前分支在dev
上,并且在最顶部。这同时也决定的Git创建分支的速度是非常快的,因为只需要增加一个指针,并不需要对工作区进行改动。转移到新的分之后,对工作区的修改和提交就是针对new
分支了,比如新提交一次后,new
指针往前移动一步,而master
指针不变。假如我们在new
上的工作完成了,就可以把new
合并到master
上。Git怎么合并呢?最简单的方法,就是直接把master
指向new
的当前提交,就完成了合并。合并完分支后,甚至可以删除new
分支。删除new
分支就是把new
指针给删掉,这样就剩下了一条master
分支。下面进行举例说明:
$ git checkout -b new
Switched to a new branch 'new'
还记得之前撤销修改的操作也是这个checkout
吗?只不过参数不同的-b
表示分支选项,这条命令相当于先创建分支再查看该分支,即:
$ git branch new
$ git checkout new
Switched to branch 'new'
前面说过可以使用git status
查看修改状态,使用git log
查看提交日志,这里我们可以用git branch
查看分支状态:
$ git branch
master
* new
*
表示当前分支,接着来试一下在当前分支进行改动操作,我还是在之前的huper.txt
中添加一句并提交:
$ git add huper.txt
$ git commit -m "update"
[new 82c2073] update
1 file changed, 1 insertion(+)
假设我们在new
分支的工作已经完成了,现在需要将两个分支合并,首先切换回master
分支:
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
这个时候HEAD
指针指向master
,而master
指向的版本并没有我们之前的提交,所以在master
中查看文本内容还是没有修改的。这个时候就需要用到分支合并:
$ git merge new
Updating 27d807c..82c2073
Fast-forward
huper.txt | 1 +
1 file changed, 1 insertion(+)
git merge
直接加上需要合并的分支就行了,合并成功后会显示所有的修改信息,并提示你此时master
的版本向前移动了,这其实就是是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。至此,new
分支的使命已经完成,可以删除它了:
$ git branch -d new
Deleted branch new (was 82c2073).
分支冲突
刚才的合并测试,都是在本地进行的,很多时候在进行协作开发的时候,并不能很顺利地合并分支,比如两个人都在各自的分支里对同一个文件有了修改然后要合并,这个时候就涉及到分支冲突的解决。我们来模拟一下这种情况,首先准备一个新的分支:
$ git checkout -b new1
Switched to a new branch 'new1'
然后在当前分支下对huper.txt
进行修改并提交:
$ git commit -m "update"
[new1 a7810b4] update
1 file changed, 1 insertion(+), 1 deletion(-)
然后切换回master
分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
我们继续在huper.txt
中进行修改然后提交:
$ git add huper.txt
$ git commit -m "update"
[master 513c245] update
1 file changed, 1 insertion(+), 1 deletion(-)
这个时候再进行merge
就会发生冲突,如下:
$ git merge new1
Auto-merging huper.txt
CONFLICT (content): Merge conflict in huper.txt
Automatic merge failed; fix conflicts and then commit the result.
Huper@DESKTOP-AT9MCJI MINGW64 /e/GitWarehouse/localgit (master|MERGING)
这个时候Git会提醒我们先解决冲突,这个时候还会发现,命令行的状态变成了MERGING
,意思就是Git在等待冲突解决完成并提交。如何查看冲突的具体信息呢?还是使用git status
命令:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: huper.txt
no changes added to commit (use "git add" and/or "git commit -a")
第一个信息说的是本地分支比远程分支领先两次提交,这个我们暂时不需要。主要看后面的提示,提示里说我们有两种选择:要么解决冲突,要么使用git merge --abort
来放弃本次合并,并且给出了存在冲突的文件路径。我们打开这个文件看看:
$ cat huper.txt
this boy is huper
and he's so stupid!
added version 1.
added version 2.
added version 3.
added stage test.
added change test append.
<<<<<<< HEAD
added branch test append2.
=======
added branch test append.
>>>>>>> new1
我们可以看到,Git非常贴心地给我们把有冲突的地方都用特殊的标记标出来了。以=======
为分隔,前面是当前分支的内容,后面是待合并分支的内容。我们把当前分支的内容删掉后再来试一下,记得删掉冲突标记,不删其实也可以合并成功,因为Git会忽视这些标记:
$ git add readme.txt
$ git commit -m "conflict fixed"
[master 59bc1cb] conflict fixed
由于我们现在的状态是MERGING
,所以只需要解决冲突并提交就行了,Git会在提交后继续进行合并。这时候的版本更新图其实如下图所示:
我们可以使用命令来在日志里查看类似上图的记录:
$ git log --graph --pretty=oneline --abbrev-commit
* b1e11a6 (HEAD -> master) uodate
* 2ed9ecb update
|\
| * a7810b4 (new1) update
* | 513c245 update
|/
* 82c2073 update
* 27d807c (origin/master) update
* 2a69d37 update
* fa57a4e update
* 24a1166 update
* f90f442 added 3
* 5cd0d8b added 2
* 143d245 added 1
* 9dc246f update
其中--graph
参数指定打印出更新图,--pretty=oneline
参数修改打印格式,只打印一行信息,--abbrev-commit
参数表示简化commit id
。最后new1
分支的使命也完成了,可以删除它了。
$ git branch -d new1
Deleted branch new1 (was a7810b4).
分支管理策略
通常在合并分支时,如果可能,Git会用Fast forward
模式,因为这种模式下的合并速度是很快的。但这种模式下,删除分支后,会丢掉分支信息。如果要强制禁用Fast forward
模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。先新建一个分支并提交一个修改,然后去切换至master
:
$ git add huper.txt
$ git commit -m "upate"
[new 4cbd89c] upate
1 file changed, 1 deletion(-)
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 5 commits.
(use "git push" to publish your local commits)
这次在merge
的时候,我们可以使用--no-ff
参数,来禁用Fast forward
。 :
$ git merge --no-ff -m "merge without fast-forword" new
Merge made by the 'recursive' strategy.
huper.txt | 1 -
1 file changed, 1 deletion(-)
之前说了,禁用Fast forward
模式下本次合并需要在当前分支创建一个新的commit
,所以需要添加提交备注。两者的区别简单来说就是:加上--no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward
合并就看不出来曾经做过合并。可以用git log
来查看以下合并时间线:
$ git log --graph --pretty=oneline --abbrev-commit
* 1b1949d (HEAD -> master) merge without fast-forword
|\
| * 4cbd89c (new) upate
|/
* b1e11a6 uodate
* 2ed9ecb update
|\
| * a7810b4 update
* | 513c245 update
|/
* 82c2073 update
* 27d807c (origin/master) update
...
Bug分支和Feature分支
在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。考虑这样的场景:你正在自己的分支new
中努力完成工作,突然接收到一个处理一个bug的紧急任务,你需要先放下手中的额任务先去解决这个bug,但是你当前在new
分支中的任务还没有提交,并且你暂时还不想提交怎么办?Git还提供了一个stash
功能,可以把当前工作现场“储藏”起来:
$ git stash
Saved working directory and index state WIP on new: 4cbd89c upate
$ git status
On branch new
nothing to commit, working tree clean
然后就可以回过头来专心解决这个bug了,假设这个bug是master
分支里的一个bug,我们就从master
上再开一个分支来处理它:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 7 commits.
(use "git push" to publish your local commits)
$ git checkout -b bug
Switched to a new branch 'bug'
然后我们修改分之后(假设删掉了一句话)再进行提交:
$ git add huper.txt
$ git commit -m "fixed a bug"
[bug 6578f1c] fixed a bug
1 file changed, 1 deletion(-)
然后回到master
分支,完成和并再删除bug分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 7 commits.
(use "git push" to publish your local commits)
$ git merge bug
Updating 1b1949d..6578f1c
Fast-forward
huper.txt | 1 -
1 file changed, 1 deletion(-)
$ git branch -d bug
Deleted branch bug (was 6578f1c).
bug解决完后就可以回到之前的分支继续进行任务了:
$ git checkout new
Switched to branch 'new'
Huper@DESKTOP-AT9MCJI MINGW64 /e/GitWarehouse/localgit (new)
$ git status
On branch new
nothing to commit, working tree clean
可以发现,这时候回到new
分支查看状态,工作区改动还是空的,并没有回到我们保存下来的状态,可以使用git stash list
命令进行查看:
$ git stash list
stash@{0}: WIP on new: 4cbd89c upate
其实stash
的保存机制就跟栈一样,我们这里有两种方法来查看,一是用git stash apply
恢复,但是恢复后,stash内容并不删除,你需要用git stash drop
来删除,这种方式也可以指定恢复和删除的内比如git stash apply stash@{0}
。另一种方式是用git stash pop
,恢复的同时把stash内容也删了:
$ git stash pop
On branch new
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: huper.txt
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (c8e36cc76d092932b7898325068d51d969f30093)
可以看到,工作区的改动已经完全恢复了。
然后是feature
分支,想象一下如果需要给master
分支里的代码添加一个新功能,我们打办法应该还是新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。我们来模拟一下,首先还是创建分支:
$ git checkout -b feature
Switched to a new branch 'feature'
$ git add huper.txt
$ git commit -m "update"
[feature 273f951] update
1 file changed, 1 insertion(+), 1 deletion(-)
大功告成,回到master
分支,打算合并并删除feature
分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 8 commits.
(use "git push" to publish your local commits)
但是如果这个时候在合并之前又决定取消这个功能呢?我们尝试下直接删除这个feature
分支:
$ git branch -d feature
error: The branch 'feature' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature'
Git考虑得很周到,因为你还没有合并,所以他会确保你不是不小心的操作,当然你可以使用-D
选项来强制删除:
$ git branch -D feature
Deleted branch (was 273f951).
总结:
新建分支 -> git branch <name>
切换分支 -> git checkout <name>
新建并切换分支 -> git branch -b <name>
查看分支状态 -> git branch
把某个分支合并到当前分支 -> git merge <name>(有冲突需要先解决冲突)
删除分支 -> git branch -d <name>(不能在当前分支删除自身。)
强制删除分支 -> git branch -D <name>
查看提交记录时间图 -> git log –graph
进制快速合并模式 -> git merge –no-ff
保存工作区现场 -> git stash
恢复现场 -> git stash pop/git stash apply+git stash drop
补充几个命令,这些不打算讲了:
查看远程库信息 -> git remote/git remote -v
从远程库的某个分支创建本地分支 -> git checkout -b <branch name> origin/<branch name>
默认会关联这两个分支。
向远程仓库推送某个本地分支 -> git push origin <branch name>
push的时候如果远程库没有对应的分支,会自动创建该分支。如果在本地删除了一个分支,也想在远程仓库删除这个分支,应该使用
git push origin :<name>
。 虽然可以指定push的参数,但是你不能随便在new
分支里去pushmaster
分支,除非你这两个分支是关联的。推送之前如果远程有更新 -> git pull
git pull
如果失败了,原因是没有指定本地dev
分支与远程origin/dev
分支的链接,需要根据提示,设置dev
和origin/dev
的链接。就是说pull
之前必须确保本地和远程的分支是关联的,比如说你在本地随便创建了一个和远程同名的分支,这是没有关联的。注意这里即使分支名不一样也可以关联,你甚至可以把本地的new
分支关联到远程的master
分支,但是建议不要这么做。