廖雪峰Git教程学习笔记
前言
关于教程作者
廖雪峰,十年软件开发经验,业余产品经理,精通Java/Python/Ruby/Visual Basic/ObjectiveC等,对开源框架有深入研究,著有《Spring 2.0核心技术与最佳实践》一书,多个业余开源项目托管在GitHub,个人官网:https://www.liaoxuefeng.com
概述
假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
创建与合并分支
在版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master
分支。HEAD
严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是当前分支。
一开始的时候,master
分支是一条线,Git用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点:
每次提交,master
分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长。
当我们创建新的分支,例如dev时,Git新建了一个指针叫dev
,指向master
相同的提交,再把HEAD
指向dev
,就表示当前分支在dev
上:
你看,Git创建一个分支很快,因为除了增加一个dev
指针,改改HEAD
的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变:
假如我们在dev
上的工作完成了,就可以把dev
合并到master
上。Git怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev
分支。删除dev
分支就是把dev
指针给删掉,删掉后,我们就剩下了一条master
分支:
下面示例演示。
首先,我们创建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.
然后提交:
$ git add readme.txt
$ git commit -m "branch test"
[dev fa80e64] branch test
1 file changed, 1 insertion(+)
现在,dev
分支的工作完成,我们就可以切换回master
分支:
$ git checkout master
Switched to branch 'master'
切换回master
分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev
分支上,而master
分支此刻的提交点并没有变:
现在,我们把dev
分支的工作成果合并到master
分支上:
$ git merge dev
Updating a1c3eb9..fa80e64
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)
git merge
命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev
分支的最新提交是完全一样的。
注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除dev
分支了:
$ git branch -d dev
Deleted branch dev (was fa80e64).
删除后,查看branch
,就只剩下master
分支了:
$ git branch
* master
因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master
分支上工作效果是一样的,但过程更安全。
解决冲突
准备新的feature1
分支,继续我们的新分支开发:
$ git switch -c feature1
Switched to a new branch 'feature1'
修改readme.txt最后一行,改为:
Creating a new branch is quick AND simple.
在feature1
分支上提交:
$ git add readme.txt
$ git commit -m "AND simple"
[feature1 fac71c7] AND simple
1 file changed, 1 insertion(+), 1 deletion(-)
切换到master
分支:
$ git checkout master
Switched to branch 'master'
在master
分支上把readme.txt文件的最后一行改为:
Creating a new branch is quick & simple.
提交:
$ git add readme.txt
$ git commit -m "& simple"
[master 8f7809f] & simple
1 file changed, 1 insertion(+), 1 deletion(-)
现在,master
分支和feature1
分支各自都分别有新的提交,变成了这样:
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
果然冲突了!Git告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交。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.txt
no changes added to commit (use "git add" and/or "git commit -a")
我们可以直接查看readme.txt的内容:
$ cat readme.txt
Git is a version control system.
Git is free software.
Git has a index called stage.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
Git用<<<<<<<,=======,>>>>>>>
标记出不同分支的内容,<<<<<<<,=======
修改,=======,>>>>>>>
之间的是合并过来的分支的内容,手动删除符号后保存。
再提交:
$ git add readme.txt
$ git commit -m "conflict fixed"
[master 9c2db59] conflict fixed
现在,master
分支和feature1
分支变成了下图所示:
使用带参数的git log
查看分支的合并情况:
$ git log --graph --pretty=oneline --abbrev-commit
--graph
是图形化, --pretty=oneline
是一行显示, --abbrev-commit
是只显示每次提交id的前几位,执行结果如下:
* 9c2db59 conflict fixed
|\
| * fac71c7 AND simple
* | 8f7809f & simple
|/
* fa80e64 branch test
* a1c3eb9 null
* 462a903 Merge branch 'master' of github.com:uidq5232/gitRepo
|\
| * a759e3f Update test.txt
| * 7a04e30 a test
* 0e437ff remove test.txt
* 789e43f add test.txt
* f086944 understand how stage works
* 92a925f this is a test
uidq5232@hzh6137u:/work/gitRepo$ git branch -d feature1
Deleted branch feature1 (was fac71c7).
最后,删除feature1
分支:
$ git branch -d feature1
Deleted branch feature1 (was fac71c7).
工作完成。
分支管理策略
Git一般会用Fast forward
模式合并分支,合并之后不会留下分支信息,这样的话删除分支后会丢失掉分支信息。强制禁用Fast forward
模式,Git就会在merge
时生成一个新的commit
,这样,从分支历史上就可以看出分支信息。
下面我们实战一下--no-ff
方式的git merge
:
首先,仍然创建并切换dev
分支:
$ git switch -c dev
Switched to a new branch 'dev'
修改readme.txt文件,并提交一个新的commit
:
$ git add readme.txt
$ git commit -m "add merge"
[master 5170837] add merge
1 file changed, 1 deletion(-)
现在,我们切换回master
:
$ git switch master
Switched to branch 'master'
准备合并dev
分支,请注意--no-ff
参数,表示禁用Fast forward
:
$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
readme.txt | 1 +
1 file changed, 1 insertion(+)
因为本次合并要创建一个新的commit
,所以加上-m
参数,把commit
描述写进去。
合并后,我们用git log
看看分支历史:
$ git log --graph --pretty=oneline --abbrev-commit
* eb4ef53 merge with no-ff
|\
| * 1d94bb3 add merge
|/
可以看到,不使用Fast forward
模式,merge
后就像这样:
分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理:
master
分支应该是非常稳定的,仅用来发布新版本,平时不能在上面干活;
dev
分支用来干活,也就是说,dev
分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev
分支合并到master
上,在master
分支发布1.0版本。
Bug分支
在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。但是当前正在dev上还有未提交的正在进行的工作,此时先用git stash
将工作现场保存起来,等修复完bug再恢复现场继续工作。
$ git stash
Saved working directory and index state WIP on dev: 1d94bb3 add merge
HEAD is now at 1d94bb3 add merge
此时用git status
查看工作区,显示是干净的
$ git status
On branch dev
nothing to commit, working directory clean
确定要在哪个分支上修复bug,假定需要在master
分支上修复代号01的bug,就从master
创建临时分支:
$ git checkout master
Switched to branch 'master'
$ git checkout -b issue-01
Switched to a new branch 'issue-01'
现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交:
$ git add readme.txt
$ git commit -m "fix bug 01"
[issue-01 79dec3f] fix bug 01
1 file changed, 1 insertion(+), 1 deletion(-)
修复完成后,切换到master
分支,并完成合并,最后删除issue-01
分支:
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff -m "merged bug fix 01" issue-01
Merge made by the 'recursive' strategy.
readme.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bug修复完毕,回到dev
分支继续干活,使用stash list
查看可知工作现场被存放在了某个地方:
$ git checkout dev
Switched to branch 'dev'
$ git stash list
stash@{0}: WIP on dev: 1d94bb3 add merge
恢复工作现场有两种方式:
一种是用git stash apply
恢复,但是恢复后,stash
内容并没有删除,需要用git stash drop
来删除;
另一种方式是用git stash pop
,恢复的同时把stash
内容也删了:
$ git stash pop
On branch dev
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
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (839c903d2a8ba253605275f85d828dc679493beb)
$ git stash list
恢复之后再次使用stash list
就看不到任何内容了。
可以多次stash
,恢复的时候,先用git stash list
查看,然后恢复指定的stash
,用命令:
$ git stash apply stash@{0}
dev
分支是早期从master
分支分出来的,master
分支上修复了bug后,在当前dev分支上也存在这个bug。
同样的bug,要在dev
上修复,只需要把4c805e2 fix bug 101
这个提交所做的修改“复制”到dev
分支。注意:我们只想复制4c805e2 fix bug 101
这个提交所做的修改,并不是把整个master
分支merge
过来。
为了方便操作,Git专门提供了一个cherry-pick
命令,让我们能复制一个特定的提交到当前分支:
$ git branch
* dev
master
$ git cherry-pick 79dec3f
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
如果提示报错,可以切换回dev后先使用git cherry-pick
命令,再使用git stash pop
命令就可以了。
强行删除分支
在master
分支上删除一个已经提交但没有合并的其它分支,则会报错:
$ git branch -d issue-101
error: The branch 'issue-101' is not fully merged.
If you are sure you want to delete it, run 'git branch -D issue-101'.
这时可以用参数 -D
强制删除:
$ git branch -D issue-101
需要注意的是,由于分支未合并,删除之后就没有任何记录了,分支上所有的修改也会丢失。
多人协作
从远程仓库克隆时,实际上Git自动把本地的master
分支和远程的master
分支对应起来了,并且,远程仓库的默认名称是origin
。
要查看远程库的信息,用git remote
:
$ git remote
origin
或者,用git remote -v
显示更详细的信息:
$ git remote -v
origin git@github.com:[用户名]/[仓库名].git (fetch)
origin git@github.com:[用户名]/[仓库名].git (push)
上面显示了可以抓取和推送的origin
的地址。如果没有推送权限,就看不到push
的地址。
推送分支
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
如果要推送其他分支,比如dev
,就改成:
$ git push origin dev
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
master
分支是主分支,因此要时刻与远程同步;dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
bug
分支只用于在本地修复bug
,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug
;
feature
分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!
抓取分支
多人协作时,大家都会往master和dev分支上推送各自的修改。
现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:
$ git clone git@github.com:[用户名]/[仓库名].git
Cloning into 'gitRepo'...
remote: Enumerating objects: 61, done.
remote: Counting objects: 100% (61/61), done.
remote: Compressing objects: 100% (34/34), done.
remote: Total 61 (delta 23), reused 59 (delta 21), pack-reused 0
Receiving objects: 100% (61/61), 5.96 KiB | 0 bytes/s, done.
Resolving deltas: 100% (23/23), done.
Checking connectivity... done.
当你的小伙伴从远程库clone
时,默认情况下,你的小伙伴只能看到本地的master
分支。使用git branch
命令可以看到:
$ git branch
* master
现在,你的小伙伴要在dev
分支上开发,就必须创建远程origin
的dev
分支到本地,于是他用这个命令创建本地dev
分支:
$ git checkout -b dev origin/dev
现在,他就可以在dev上继续修改,然后,时不时地把dev
分支push
到远程:
$ git add readme.txt
$ git commit -m "add readme"
[dev 6a50e8b] add readme
1 file changed, 1 insertion(+)
$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 12 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 345 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To git@github.com:uidq5232/gitRepo.git
90025cd..6a50e8b dev -> dev
你的小伙伴已经向origin/dev
分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:
$ git add readme.txt
$ git commit -m "add new readme"
[dev fc40080] add new readme
1 file changed, 2 insertions(+)
$ git push origin dev
推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用git pull
把最新的提交从origin/dev
抓下来,然后,在本地合并,解决冲突,再推送:
$ git pull
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
也失败了,原因是没有指定本地dev
分支与远程origin/dev
分支的链接,根据提示,设置dev
和origin/dev
的链接:
$ git branch --set-upstream-to=origin/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
再pull:
$ git pull
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
这回git pull
成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push
就成功了:
$ git add readme.txt
$ git commit -m "fix read conflict"
[dev 762794f] fix read conflict
$ git push origin dev
Counting objects: 6, done.
Delta compression using up to 12 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 684 bytes | 0 bytes/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To git@github.com:uidq5232/gitRepo.git
6a50e8b..762794f dev -> dev
因此,多人协作的工作模式通常是这样:
首先,可以试图用git push origin <branch-name>
推送自己的修改;
如果推送失败,则因为远程分支比你的本地更新,需要先用git pull
试图合并;
如果合并有冲突,则解决冲突,并在本地提交;
没有冲突或者解决掉冲突后,再用git push origin <branch-name>
推送就能成功!
如果git pull
提示no tracking information
,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>
建立连接。
这就是多人协作的工作模式。