- 为什么在Mercuial或者git中,创建分支比SVN分支容易
- 分布式版本控制系统和集中式版本管理系统有什么各自的缺点和优点?
- 你能描述github flow和gitflow各自的工作流?
- 什么是rebase?
- 在Mercurial或者git中合并比SVN中容易;
SVN
并没有一个叫做分支的概念,当你在 SVN 中使用所谓的分支的时候,只不过是创建了一个新的文件夹,然后将所有的文件拷贝进去。如果你仅仅是创建新的分支,然后做一些修改并且合并到之前的分支,最后删掉这个分支,这个没有任何大的问题。但是如果你想
- 保持这个分支
- 如果想从别的分支合并过来,而且该分支也做了一些修改
那么你可能会遇到一些问题,处理起来非常耗时。那么 git
是如何处理分支的问题的呢?
本质上来讲,git
是一个定制的文件数据库,它用 .git
文件夹的保存当前的目录下所有的文件的信息,并且使用树状结构来管理它们。
首先我们可以看看 .git
目录是怎样的,截图如下。
在这里我们只关注 objects
和 refs/heads
两个文件夹,object
文件夹中主要存放了三种类型数据
- 文件 (blob)
- 文件夹 (tree)
- 提交 (commit)
- 对于文件,读取文件内容并且使用
Sha256
提取摘要作为文件名,git
将前两个字符作为文件夹名字; - 对于文件夹,它是一系列这样三元组
(<name> <sha256> <type>)
的集合。name
可以是文件的名字,也可以是子文件名字;sha256
比较容易理解,这些信息都保存在ojects
目录下;type
主要分为两类:blob
和tree
两种。 - 对于提交,主要是由下面的信息组成
tree <tree sha256>
parent <previous commit sha256>
<commit message>
提交信息也通过 sha256
提取摘要并且将这些信息写入到相应的文件中。通过提交信息中 parent
信息,我们可以将 git
仓库中的文件按照一定结构组织起来。
假设我们的仓库中文件组织是这样的
.
+-- hello.txt
| +tree
| -- git.txt
体现在 git
文件数据库中是这样的:
那么 git
是如何知道每个分支处在何处呢?答案就在 refs\heads
文件夹中,每个文件就是一个分支,从上面的截图中,这个仓库只有一个 master
分支,这个文件的内容就是某一个提交的 sha256
的值。所以在 git
中创建一个分支非常容易,只需要在 refs\heads
目录下创一个文件,文件名就是分支的名称,内容是当前提交的 sha256
值。git
是如何知道但当前的提交的呢? 答案在 .git\HEAD
这文件, 比如当前这个文件内容是 ref: refs/heads/master
,它表明目前是处在 master
分支上。
集中式版本管理的优点
- 搭建起来比较方便
- 有很高的透明度
- 方便管理员控制流程
但是也有下面的缺点
- 如果主服务器宕机,所有人都无法在使用版本控制服务
- 远程提交比较慢
- 不合理的更改可能导致开发环境的不可用
- 如果主服务器的数据库损坏,就导致所有的提交历史无法查询
分布式版本管理的优点
- 因为可以本地提交, 所有的历史都能查询
- 无需访问远端服务器
- 可以持续 push 修改
- 节省时间
- 对于分布式的开发这非常友好
分布式版本管理的缺点
- 别人的修改可能不透明
- 文件的冲突修改可能导致开发进度变慢
- 分布式系统允许克隆整个仓库,可能有安全隐患
- 二进制文件之间的 diff 非常困难。
github flow
github flow 以部署为中心的开发模式,功能简单可靠,持续、安全和高效的部署,以Pull Request
为中心
master
分支保持可部署的状态;- 从
master
分支切换出功能分支,并进行开发工作; - 在
github
仓库创建同名的远端分支,并push
到远端分支上; - 在合并时候时候,提交
Pull Request
,以Pull Request
为交流工具; - 其他开发者对
Pull Request
进行Review
git flow
master
分支和develop
分支贯穿于整个开发过程
- master: 软件可以正常运行的状态,不允许直接在该分支上直接开发,其他分支的开发工作合并到
master
分支上,在发布的时候附加上tag
号 - develop: 是开发过程中的中心分支,所有开发工作以该分支作为起点;
- feature: 是开发人员直接工作的分支,在
feature
分支完成开发工作后,向develop
分支提交Pull Request
请求。 - release: 处理发布前相关处理,变更版本号,或者提交到beta环境进行测试,该分支不允许有较大的改动;
- hot-fix: 对于已经
release
分支,如果发现出现bug
修复,则将在以该tag
号为基础,创建hot-fix
分支,并且修改完后,分别向deveop
和master
分支请求合并,并更新tag
号。
在很多人看来merge
和rebase
的功能是类似的,都是从别的分支冲获取提交记录。这种理解是错误的,git
很重要的一点就是给大众看到这个repo代码在整个生命周期内的活动,这个记录都是有语义化。 在我们打算merge
一个分支的时候,需要思考一下这个问题:这个分支的作用是什么?
- 如果这个分支的是本地的,而且仅仅是为了防止待合并的分支在开发过程中不稳定,那么这个分支就不应该出现的在整个仓库的历史记录中,则使用
fast forward
方式merge
该分支。但是在开发过程中,如果主分支已经做了相关的提交,也就是当前主分支的头已经不是带合并分支基,那么就要在待合并的分支上进行变基rebase
,然后使用fast forward
方式进行合并。 - 同样如果该分支是具有特定意义的,比如
feature
或者bugfix
分支,那么这个分支一定要保留在历史记录中,使用no fast forward
进行合并分支。
rebase
主要用于有当前的工作的分支的基
太老了,需要更新以获得最新的提交。还有rebase
也经常用在修正分支的commit
提交记录,使之变得更加有清晰明了。
注意:千万不要在一个共享的分支上做rebase
操作,只能reabse
私有化的分支。
在 SVN 中将整个系统的迭代当成一个版本(Version), 在每个节点为每个文件生成文件快照。如果说整个历史是线性的,那么合并分并没有难度,但是如何要去合并两个独立的分支,那么 SVN 需要比较两个版本,然后通过三路对比(Three-Way Comparsion) 的方式来对比:
- 最近的共有的版本
- 两个要合并的版本
有些文件行在一个版本中修改了,但是没有在另一个中修改,合并比较简单。但是如果某一个行的修改发生在所有的版本中, SVN 就会报错,需要人工介入。
与之相反的是,Mercurial
或者 Git
使用修改数据集(ChangeSets) 来记录修改而不是版本。整个仓库是用一个ChangeSets 组装成的树结构,每个节点依赖于其父节点,而父节点也会有若干个子节点,根节点代表了一个空目录。换句话说,git或者hg 是这边表达的
- 首先我是一个空节点
- 每次提交就得到了一个 patch
这样每次在合并的时候,git/hg 不仅知道每个分支当前的是什么情况,而且还知道它们之前的转移的过程,这样合并的过程就简单多了。