Git

Git学习笔记 07

分支管理

Posted by Huper on December 15, 2017

之前的学习过程里,所有的操作都是在主分支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会在提交后继续进行合并。这时候的版本更新图其实如下图所示:

none

我们可以使用命令来在日志里查看类似上图的记录:

$ 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分支的链接,需要根据提示,设置devorigin/dev的链接。就是说pull之前必须确保本地和远程的分支是关联的,比如说你在本地随便创建了一个和远程同名的分支,这是没有关联的。注意这里即使分支名不一样也可以关联,你甚至可以把本地的new分支关联到远程的master分支,但是建议不要这么做。