概述
什么是“版本控制”?我为什么要关心它呢? 版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 在本书所展示的例子中,我们对保存着软件源代码的文件作版本控制,但实际上,你可以对任何类型的文件进行版本控制。
三种分类(演化)
- 本地版本控制系统
- 集中化版本控制系统
- 分布式版本控制系统
分布式版本控制系统(Distributed Version Control System,简称 DVCS)。 在这类系统中,像 Git、Mercurial、Bazaar 以及 Darcs 等,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
更进一步,许多这类系统都可以指定和若干不同的远端代码仓库进行交互。籍此,你就可以在同一个项目中,分别和不同工作小组的人相互协作。 你可以根据需要设定不同的协作流程,比如层次模型式的工作流,而这在以前的集中式系统中是无法实现的。
Git基础
1. 直接记录快照,而非差异比较
Git 和其它版本控制系统(包括 Subversion 和近似工具)的主要差别在于 Git 对待数据的方法。概念上来区分,其它大部分系统以文件变更列表的方式存储信息。这类系统(CVS、Subversion等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。Git 更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。 为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个快照流。
2. 近乎所有操作都是本地执行
在 Git 中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。 如果你习惯于所有操作都有网络延时开销的集中式版本控制系统,Git 在这方面会让你感到速度之神赐给了 Git 超凡的能量。 因为你在本地磁盘上就有项目的完整历史,所以大部分操作看起来瞬间完成。
3. Git 保证完整性
Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。
Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来。
4. Git 一般只添加数据
你执行的 Git 操作,几乎只往 Git 数据库中增加数据。 很难让 Git 执行任何不可逆操作,或者让它以任何方式清除数据。 同别的 VCS 一样,未提交更新时有可能丢失或弄乱修改的内容;但是一旦你提交快照到 Git 中,就难以再丢失数据,特别是如果你定期的推送数据库到其它仓库的话。
三种状态
Git 有三种状态,你的文件可能处于其中之一:
- 已提交(committed)表示数据已经安全的保存在本地数据库中
- 已修改(modified)表示修改了文件,但还没保存到数据库中
- 已暂存(staged)表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中
三个工作区域
# 加上远程仓库算四个
Workspace:工作区
Index / Stage:暂存区
Repository:仓库区(或本地仓库)
Remote:远程仓库
基本的 Git 工作流程如下:
- 在工作目录中修改文件。
- 暂存文件,将文件的快照放入暂存区域。
- 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。
安装和使用
安装(略)
建议保留Git Bash
GUI工具
常用工具推荐
- IntelliJ IDEA 自带插件
- Source Tree 号称最好用的图形工具
- TortoiseGit 小乌龟,使用SVN的时候经常用
SSH
1. 验证SSH key是否已存在
$ cd ~/.ssh
$ ls
看是否存在.pub文件,如果已存在可跳过第2步
2. 生成新的SSH key
$ ssh-keygen -t rsa -C "[email protected]"
参数说明:
- -t 指定密钥类型,默认是 rsa ,可以省略
- -C 设置注释文字,比如邮箱
- -f 指定密钥文件存储文件名
3. 添加SSH key到Git服务
$ cat ~/.ssh/id_rsa.pub | clip
也可以用文本编辑器打开pub文件后复制字符
打开Git的SSH配置页,将复制的SSH key添加进去
基本命令
# 1. 初始化本地仓库
$ git init
# 2. 绑定一个远程仓库,并将远程仓库别名设置为origin
$ git remote add origin '远程git仓库地址'
# 3. 从远程仓库获取最新代码
$ git pull origin master
# 4. 拉取远程仓库到本地
$ git clone '远程git仓库地址'
# 5. 查看当前改动了哪些文件
$ git status
# 6. 将改动的这个文件提交到暂存区
$ git add '一个文件的路径'
# 7. 将文件夹下所有的改动提交到暂存区
$ git add '一个文件夹'
# 8. 将所有改动的文件提交到暂存区
$ git add .
# 9. 对本次要提交的的文件进行说明
$ git commit -m '提交说明'
# 10. 将修改的文件提交到远程仓库
$ git push origin master
# 11. 创建一个名为dev的新分支,并切换到dev分支,dev的内容取决于你再哪个分支上执行该命令
$ git checkout -b dev
# 12. 创建一个名为dev的新分支,但是不做切换动作。dev的内容取决于你在哪个分支上执行该命令
$ git branch dev
# 13. 切换到dev分支,切换分支要保证工作区纯净
$ git checkout dev
# 14. 将dev分支的改动合并到目标分支。目标分支为你当前工作区所在的分支
$ git merge dev
# 15. 查看所有的分支
$ git branch -a
# 16. 删除本地dev分支
$ git branch -d dev
# 17. 删除origin对应的远程仓库的dev分支
$ git push --delete origin dev
# 18. 撤销一个文件的改动
$ git checkout -- '文件路径'
# 19. 将一个文件撤出暂存区,保留修改
$ git reset '文件路径'
# 20. 未push到远程分支时修改commit message的方法
$ git commit --amend -m '消息内容'
# 21. 删除远程标签
$ git push origin :refs/tags/标签名
高级用法
- 需求做了一半告诉你需求有变化,不再做了,如果你此时还没有进行commit
$ git reset --hard
- 代码已经commit ,还未进行push
$ git reset --hard '上一次提交的版本号'
- 代码已经commit,但是发现不该提交的文件也在里面
$ git reset '上一次提交的版本号'
将提交撤销,把所有文件从本地仓库退出保持自己修改的状态,重置暂存区,但工作区不变
- 代码已经commit ,提交说明写的不太好,想重新修改下提交说明
$ git reset --soft '上一次提交的版本号'
不涉及暂存区和工作区,可以直接执行git commit -m '说明'
- 代码改了半天,发现自己改错分支了。
没有冲突的情况下可以试着直接执行git checkout '对的分支'
,如果提示有错,再使用以下方法
$ git add .
$ git stash
$ git checkout '对的分支'
$ git stash pop
- 代码不但做错分支了,而且还提交到远程了。
$ git checkcout '错误分支'
$ git reset --soft '上一次提交的版本号'
$ git stash
$ git checkout '对的分支'
$ git stash pop
$ git commit -m "版本说明"
$ git push origin '对的分支'
$ git checkout 错的分支
$ git push origin 错的分支 --force
- 已经发布一期,二期内容还未完全开发完。客户急着想要二期的一个功能。该功能二期已经开发完。可在一期的分支上执行:
$ git cherry-pick '该功能所形成的版本号'
如果有多次提交可按 git cherry-pick
功能版本号1 功能版本号2
注意,抽取功能过程中可能会产生冲突。需要解决冲突后再继续cherry-pick
- 合并代码某个冲突文件已经搞不太清楚是保留谁的版本合适。
$ git checkout --conflict=diff3 '已经merge冲突的某个文件'。
此命令每个冲突的文件会产生三段标志,第一段为自己修改的。第二段是自己修改前的。第三段是别人修改的。
- 撤销已提交的某个commit
$ git revert '提交的版本号'
此命令会新建一个反转的commit,一般用于已提交远程仓库的回退。
- 将某个文件回退到指定版本
$ git checkout '版本号' -- '文件路径'
- 之前重置了一个不想保留的提交,但是现在又想要回滚。
# 获取所有操作历史
$ git reflog
# 重置到相应提交
$ git reset HEAD@{4}
# ……或者……
$ git reset --hard <提交的版本号>
常用工作流
- Git Flow
- GitHub Flow
- GitLab Flow
这三种工作流程,有一个共同点:都采用"功能驱动式开发"(Feature-driven development,简称FDD)。
它指的是,需求是开发的起点,先有需求再有功能分支(feature branch)或者补丁分支(hotfix branch)。完成开发后,该分支就合并到主分支,然后被删除。
1. Git flow
1.1 两个特点
首先,项目存在两个长期分支。
- 主分支
master
- 开发分支
develop
前者用于存放对外发布的版本,任何时候在这个分支拿到的,都是稳定的分布版;后者用于日常开发,存放最新的开发版。
其次,项目存在三种短期分支。
- 功能分支(feature branch)
- 补丁分支(hotfix branch)
- 预发分支(release branch)
一旦完成开发,它们就会被合并进develop
或master
,然后被删除。
1.2 优缺点
Git flow的优点是清晰可控,缺点是相对复杂,需要同时维护两个长期分支。大多数工具都将master当作默认分支,可是开发是在develop分支进行的,这导致经常要切换分支,非常烦人。
更大问题在于,这个模式是基于"版本发布"的,目标是一段时间以后产出一个新版本。但是,很多网站项目是"持续发布",代码一有变动,就部署一次。这时,master分支和develop分支的差别不大,没必要维护两个长期分支。
2. Github flow
Github flow是Git flow的简化版,专门配合"持续发布"。它是 Github.com 使用的工作流程
2.1 流程
它只有一个长期分支,就是master
,因此用起来非常简单。官方推荐的流程如下:
第一步:根据需求,从
master
拉出新分支,不区分功能分支或补丁分支。
第二步:新分支开发完成后,或者需要讨论的时候,就向master
发起一个pull request(简称PR)。
第三步:Pull Request既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。对话过程中,你还可以不断提交代码。
第四步:你的Pull Request被接受,合并进master
,重新部署后,原来你拉出来的那个分支就被删除。(先部署再合并也可。)
2.2 优缺点
Github flow 的最大优点就是简单,对于"持续发布"的产品,可以说是最合适的流程。
问题在于它的假设:master
分支的更新与产品的发布是一致的。也就是说,master
分支的最新代码,默认就是当前的线上代码。
可是,有些时候并非如此,代码合并进入master
分支,并不代表它就能立刻发布。比如,苹果商店的APP提交审核以后,等一段时间才能上架。这时,如果还有新的代码提交,master
分支就会与刚发布的版本不一致。另一个例子是,有些公司有发布窗口,只有指定时间才能发布,这也会导致线上版本落后于master
分支。
上面这种情况,只有master
一个主分支就不够用了。通常,你不得不在master
分支以外,另外新建一个production
分支跟踪线上版本。
3. Gitlab flow
Gitlab flow是 Git flow 与 Github flow 的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是 Gitlab.com 推荐的做法。
3.1 上游优先
Gitlab flow 的最大原则叫做"上游优先"(upsteam first),即只存在一个主分支master
,它是所有其他分支的"上游"。只有上游分支采纳的代码变化,才能应用到其他分支。
Chromium项目就是一个例子,它明确规定,上游分支依次为:
- Linus Torvalds的分支
- 子系统(比如netdev)的分支
- 设备厂商(比如三星)的分支
3.2 持续发布
Gitlab flow 分成两种情况,适应不同的开发流程。
对于"持续发布"的项目,它建议在master
分支以外,再建立不同的环境分支。比如,"开发环境"的分支是master
,"预发环境"的分支是pre-production
,"生产环境"的分支是production
。
开发分支是预发分支的"上游",预发分支又是生产分支的"上游"。代码的变化,必须由"上游"向"下游"发展。比如,生产环境出现了bug,这时就要新建一个功能分支,先把它合并到master
,确认没有问题,再cherry-pick
到pre-production
,这一步也没有问题,才进入production
。
只有紧急情况,才允许跳过上游,直接合并到下游分支。
3.3 版本发布
对于"版本发布"的项目,建议的做法是每一个稳定版本,都要从master
分支拉出一个分支,比如2-3-stable
、2-4-stable
等等。
以后,只有修补bug,才允许将代码合并到这些分支,并且此时要更新小版本号。
4. AoneFlow
另辟蹊径的 AoneFlow
在 AoneFlow 上你能看到许多其他分支模式的影子。它基本上兼顾了 TrunkBased 的“易于持续集成”和 GitFlow 的“易于管理需求”特点,同时规避掉 GitFlow 的那些繁文缛节。
看一下具体套路。AoneFlow 只使用三种分支类型:主干分支、特性分支、发布分支,以及三条基本规则。
4.1 规则一,开始工作前,从主干创建特性分支。
AoneFlow 的特性分支基本借鉴 GitFlow,没有什么特别之处。每当开始一件新的工作项(比如新的功能或是待解决的问题)的时候,从代表最新已发布版本的主干上创建一个通常以feature/前缀命名的特性分支,然后在这个分支上提交代码修改。也就是说,每个工作项(可以是一个人完成,或是多个人协作完成)对应一个特性分支,所有的修改都不允许直接提交到主干。
4.2 规则二,通过合并特性分支,形成发布分支。
GitFlow 先将已经完成的特性分支合并回公共主线(即开发分支),然后从公共主线拉出发布分支。TrunkBased 同样是等所有需要的特性都在主干分支上开发完成,然后从主干分支的特定位置拉出发布分支。而 AoneFlow 的思路是,从主干上拉出一条新分支,将所有本次要集成或发布的特性分支依次合并过去,从而得到发布分支。发布分支通常以release/前缀命名。
4.3 规则三,发布到线上正式环境后,合并相应的发布分支到主干,在主干添加标签,同时删除该发布分支关联的特性分支。
当一条发布分支上的流水线完成了一次线上正式环境的部署,就意味着相应的功能真正的发布了,此时应该将这条发布分支合并到主干。为了避免在代码仓库里堆积大量历史上的特性分支,还应该清理掉已经上线部分特性分支。与 GitFlow 相似,主干分支上的最新版本始终与线上版本一致,如果要回溯历史版本,只需在主干分支上找到相应的版本标签即可。
除了基本规则,还有一些实际操作中不成文的技巧。比如上线后的 Hotfix,正常的处理方法应该是,创建一条新的发布分支,对应线上环境(相当于 Hotfix 分支),同时为这个分支创建临时流水线,以保障必要的发布前检查和冒烟测试能够自动执行。但其实还有一种简便方法是,将线上正式环境对应的发布分支上关联的特性分支全部清退掉,在这个发布分支上直接进行修改,改完利用现成的流水线自动发布。如果非得修一个历史版本的 Bug 怎么办呢?那就老老实实的在主干分支找到版本标签位置,然后从那个位置创建 Hotfix 分支吧。
分支开发规范
- 分支的定义(master、develop、release、hotfix、feature)
- 分支命名规范
- checkout、merge request流程
- 提测流程
- 上线流程
- Hotfix流程
参考资料
- Git官方Book
- Git参考目录
- Git工作流程
- 常用 Git 命令清单
- 在阿里,我们如何管理代码分支