git干货系列:(五)多人协同工作之分支管理

分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习 Git 的时候,另一个你正在另一个平行宇宙里努力学习 SVN 。如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了 Git 又学会了 SVN !

正文

分支简介

为了真正理解 Git 处理分支的方式,我们需要回顾一下 Git 是如何保存数据的。

Git 保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。在进行提交操作时, Git 会保存一个提交对象( commit object )。知道了 Git 保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master 。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操作中自动向前移动。

Git 的 “master” 分支并不是一个特殊分支。它就跟其它分支完全没有区别。 之所以几乎每一个仓库> 都有 master 分支,是因为 git init 命令默认创建它,并且大多数人都懒得去改动它。

分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。

但 Git 的分支是与众不同的,无论创建、切换和删除分支, Git 在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。

分支创建

Git 是怎么创建新分支的呢? 很简单,它只是为你创建了一个可以移动的新的指针。 比如,创建一个 testing 分支, 你需要使用 git branch 命令:

$ git branch testing

这会在当前所在的提交对象上创建一个指针。

那么, Git 又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。 请注意它和许多其它版本控制系统(如 Subversion 或 CVS)里的 HEAD 概念完全不同。 在 Git 中,它是一个指针,指向当前所在的本地分支(译注:将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在 master 分支上。 因为 git branch 命令仅仅 创建 一个新分支,并不会自动切换到新分支中去。

你可以简单地使用 git log 命令查看各个分支当前所指的对象。 提供这一功能的参数是 --decorate 。

$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project

正如你所见,当前 “master” 和 “testing” 分支均指向校验和以 f30ab 开头的提交对象。

分支切换

要切换到一个已存在的分支,你需要使用 git checkout 命令。 我们现在切换到新创建的 testing 分支去:

$ git checkout testing

这样 HEAD 就指向 testing 分支了。

上面的创建分支和切换分支命令可以合起来用下面这个命令来替代。

$ git checkout -b testing

那么,这样的实现方式会给我们带来什么好处呢? 现在不妨再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'

如图所示,你的 testing 分支向前移动了,但是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象。 这就有意思了,现在我们切换回 master 分支看看:

$ git checkout master

这条命令做了两件事。 一是使 HEAD 指回 master 分支,二是将工作目录恢复成 master 分支所指向的快照内容。 也就是说,你现在做修改的话,项目将始于一个较旧的版本。 本质上来讲,这就是忽略 testing 分支所做的修改,以便于向另一个方向进行开发。

可以使用 git branch 命令查看当前分支,注意前面带 * 的表示当前分支

Note

分支切换会改变你工作目录中的文件

在切换分支时,一定要注意你工作目录里的文件会被改变。 如果是切换到一个较旧的分支,你的工作目> 录会恢复到该分支最后一次提交时的样子。 如果 Git 不能干净利落地完成这个任务,它将禁止切换分支。

合并分支(快速合并)

假如我们在 testing 上的工作完成了,就可以把 testing 合并到 master 上。 Git 怎么合并呢?最简单的方法,就是直接把 master 指向 testing 的当前提交,就完成了合并,这里你需要使用 git merge 命令

$ git merge testing
Updating 64ba18a..760118b
Fast-forward
 hello.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt

git merge 命令用于合并指定分支到当前分支。合并后,再查看内容,就可以看到,和 testing 分支的最新提交是完全一样的。

注意到上面的 Fast-forward 信息, Git 告诉我们,这次合并是“快进模式”,也就是直接把 master 指向 testing 的当前提交,所以合并速度非常快。

当然,也不是每次合并都能 Fast-forward ,我们后面会讲其他方式的合并。

删除分支

合并完分支后,甚至可以删除 dev 分支。删除 dev 分支就是把 dev 指针给删掉,删掉后,我们就剩下了一条 master 分支,这里需要使用 git branch -d 命令来删除分支

$ git branch -d testing
Deleted branch testing (was 760118b).

分支合并冲突

人生不如意之事十之八九,合并分支往往也不是一帆风顺的。

准备新的 dev 分支,继续我们的新分支开发:

$ git checkout -b dev
Switched to a new branch 'dev'

修改 README.md 内容,添加一样内容”day day up~”,在 dev 分支上提交:

$ git commit -am "one commit"
[dev 6a6a08e] one commit
 1 file changed, 1 insertion(+)

切换到 master 分支:

$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

Git 还会自动提示我们当前 master 分支比远程的 master 分支要超前1个提交。

在 master 分支上把 README.md 文件的最后改为 good good study ,然后提价

$ git commit -am "two commit"
[master 75d6f25] two commit
 1 file changed, 1 insertion(+)

现在, master 分支和 dev 分支各自都分别有新的提交,变成了这样:

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:

$ git merge dev
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 status 也可以告诉我们冲突的文件:

$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
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`的内容:  
``` bash
$ cat README.md
#gitLearn
<<<<<<< HEAD
good good study
=======
day day up
>>>>>>> dev

Git用 <<<<<<< , ======= , >>>>>>> 标记出不同分支的内容,我们修改如下后保存:

#gitLearn
good good study
day day up

再提交:

$ git commit -am 'merge commit'
[master 9a4d00b] merge commit

现在, master 分支和 dev 分支变成了下图所示:

用带参数的 git log 也可以看到分支的合并情况:

$ git log --graph --pretty=oneline --abbrev-commit
*   9a4d00b merge commit
|\
| * 6a6a08e one commit
* | 75d6f25 two commit
|/
* ae06dcf 123
* 760118b test
*   64ba18a test
|\
| *   4392848 Accept Merge Request #1 test : (dev -> master)
| |\
| | * a430c4b update README.md
| |/
| * 88ec6d7 Initial commit
* 32d11c8 update README.md
* 8d5acc1 new file README
* e02f115 Initial commit

```  
最后,删除`feature1`分支:  
``` bash
$ git branch -d dev
Deleted branch dev (was 6a6a08e).

合并分支(普通合并)

通常,合并分支时,如果可能,Git会用 Fast forward 模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用 Fast forward 模式, Git 就会在 merge 时生成一个新的 commit ,这样,从分支历史上就可以看出分支信息。

下面我们实战一下 --no-ff 方式的 git merge :

首先,仍然创建并切换 dev 分支:

$ git checkout -b dev
Switched to a new branch 'dev'

修改README.md文件,并提交一个新的commit:

$ git commit -am 'submit'
[dev fee6025] submit
 1 file changed, 1 insertion(+)

现在,我们切换回 master :

$ git checkout 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.md | 1 +
 1 file changed, 1 insertion(+)

因为本次合并要创建一个新的commit,所以加上 -m 参数,把commit描述写进去。

合并后,我们用 git log 看看分支历史:

$ git log --graph --pretty=oneline --abbrev-commit
*   b98f802 merge with no-ff
|\
| * fee6025 submit
|/
*   9a4d00b merge commit
...

可以看到,不使用 Fast forward 模式,merge后就像这样:

分支管理策略

实际公司开发的时候一般3个分支就可以了:

  1. mster 主分支用来发布
  2. dev 日常开发用的分支
  3. bug 修改bug用的分支

首先, master 分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;

干活都在 dev 分支上,也就是说, dev 分支是不稳定的,到某个时候,比如1.0版本发布时,再把 dev 分支合并到 master 上,在 master 分支发布1.0版本,你和你的小伙伴们每个人都在 dev 分支上干活,每个人都有自己的分支,时不时地往 dev 分支上合并就可以了;

bug 分支用来处理日常bug,搞定后合到dev分支即可;

假设远程公共仓库,有一个 master 和一个 dev 分支,进行多人协作开发时候(每个人的公钥必须加入到远程账号下,否则无法 push ), 每个人都应该 clone 一份到本地。 但是 clone 的只是 master ,如果远程的 master 和 dev 一样,没关系;如果不一致,则需要 clone 出 dev分支 git checkout -b dev origin/dev 之后每个人在本地的 dev 分支上独自开发(最好不要在 mast 上开发), 开发完成之后 push 到远程 dev , git push origin dev 。 之后审核人再确定是否合并 dev 到 master 。

团队多人开发协作

当你从远程仓库克隆时,实际上Git自动把本地的 master 分支和远程的 master 分支对应起来了,并且,远程仓库的默认名称是 origin 。

要查看远程库的信息,用 git remote :

$ git remote
origin

或者,用 git remote -v 显示更详细的信息:

$ git remote -v
origin  git@git.coding.net:tengj/gitLearn.git (fetch)
origin  git@git.coding.net:tengj/gitLearn.git (push)

上面显示了可以抓取和推送的 origin 的地址。如果没有推送权限,就看不到push的地址。

推送分支

推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样, Git 就会把该分支推送到远程库对应的远程分支上:

$ git push origin master

如果要推送其他分支,比如 dev ,就改成:

$ git push origin dev

抓取分支

多人协作时,大家都会往 master 和 dev 分支上推送各自的修改。

现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把 SSH Key 添加到 GitHub )或者同一台电脑的另一个目录下克隆:

$ git clone git@git.coding.net:tengj/gitStudy.git
Cloning into 'gitStudy'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.
Checking connectivity... done.
```  
当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的`master`分支。不信可以用`git branch`命令看看:
``` bash
$ git branch
* master

现在,你的小伙伴要在 dev 分支上开发,就必须创建远程 origin 的 dev 分支到本地,于是他用这个命令创建本地 dev 分支(程分支dev要先创建)。

$ git checkout -b dev
git

创建dev分之后,先同步远程服务器上的数据到本地

$ git fetch origin
From git.coding.net:tengj/gitStudy
 * [new branch]      dev        -> origin/dev

现在,他就可以在 dev 上继续修改,然后,时不时地把 dev 分支 push 到远程:

$ git commit -am 'test'
[dev c120ad6] test
 1 file changed, 1 insertion(+)
$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 262 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@git.coding.net:tengj/gitStudy.git
   65c05aa..c120ad6  dev -> dev

你的小伙伴已经向 origin/dev 分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:

$ git push origin dev
To git@git.coding.net:tengj/gitStudy.git
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to 'git@git.coding.net:tengj/gitStudy.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用 git pull 把最新的提交从 origin/dev 抓下来,然后,在本地合并,解决冲突,再推送:

$ git pull origin dev
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From git.coding.net:tengj/gitStudy
 * branch            dev        -> FETCH_HEAD
   b7b87f4..f636337  dev        -> origin/dev
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
Automatic merge failed; fix conflicts and then commit the result.

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用 git push origin branch-name 推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用 git pull 试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用 git push origin branch-name 推送就能成功!

如果 git pull 提示“no tracking information”,则说明本地分支和远程分支的链接关系没有创建,用命令 git branch --set-upstream-to branch-name origin/branch-name 。

这就是多人协作的工作模式,一旦熟悉了,就非常简单。

总结

到此, Git 分支管理就学完了,整理一下所学的命令,大体如下:

git branch           查看当前分支
git branch -v        查看每一个分支的最后一次提交
git branch -a        查看本地和远程分支的情况
git branch --merged  查看已经与当前分支合并的分支
git branch --no-merged 查看已经与当前分支未合并的分支
git branch -r        查看远程分支
git branch dev       创建分支 dev
git checkout dev     切换到分支dev
git checkout -b dev  创建并切换分支dev
git merge dev        名称为dev的分支与当前分支合并
git branch -d dev    删除分支dev
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值