写在前面

在我建议你使用git前,首先要给你泼一盆冷水,见博文我痛恨git的10个理由,以下为文中的片段:

Git 是一个源代码版本控制系统,正在迅速成为开源项目的标准。它有一个强大的分布式模型,允许高级用户用分支来处理各种棘手的问题和改写历史记录。但是,要学习 Git 是需要付出更多的努力,让人不爽的命令行接口以及 Git 是如此的忽视它的使用者。

如果你是一个架构师,那么 Git 是很棒的。但对用户来说它很糟糕,已经有不少人在为 Git 编写一些简化的接口,例如gitflow

如果 Git 的强大之处是分支和合并,那么它的弱点就是让简单的任务变得非常复杂。

git是如此的强大,强大到你都不知道该怎么使用它才好了…

本文提出了一个“git该怎么在科研实践中轻松的使用”的一个模型,包含众多的规范来约束你git的使用方法。所谓没有规矩不成方圆,遵循了模型中的约束条件后,你的git仓库会变得清晰、优雅起来;你会享受你那结构清晰的network图(而不是之前那乱糟糟的图形),而你的合作者也能从你的提交序列中清楚的认识到你的工作思路。

当然,每次使用git的时候都要小心翼翼的遵循这些规范是一件困难的事情。所以我开发了一套脚本, gg(good use of git),来傻瓜式的实现下述的模型

=============模型开始的分割线===============

模型思想基于A successful Git branching model,结合本人的学习工作实际和脑洞,升级为此模型。 声明:本文只讨论模型,并不讨论模型的具体实现过程,技术密集文章恐惧患者请放心食用。

引言:版本控制

什么是版本控制?

manual version contral

图示即为(手动的)版本控制

我们在完成一件工作的时候,一定不会是“量子化”的完成,总会经历一些中间阶段。 如果我们将这行中间过程分别保存下来,并且可以形成索引,得以方便的重现,就是版本控制。 版本控制有助于我们掌握自己工作的进度和结构,而且给我们提供了一个安全的试错空间。 我们可以放心大胆的进行实验,最后可以接受实验结果并把实验过程并入自己的工作中,或者放弃试验结果,从实验前的状态重新开始。

常见的版本控制程序有git,svn等,本文以git的分支特性为基础,介绍一个实用的git分支模型,欢迎大家使用和讨论。(为缩小学习成本,文中只保留版本控制的最核心概念,已经学习过git的不要在意一些不严谨的细节。)

模型术语、假设、约定和其用途

术语:

  1. 状态:现在工作所使用的所有文件的集合被版本控制程序记录了下来称为一个状态。(我的活儿干到这了,保存一下,来张“快照”)
  2. 提交:从一个状态,经过一些工作,到达了另外一个状态,称为一次提交。(又干了点活,再保存一下,再来张“快照”)
  3. 分支名:多个提交形成一个提交链,给链最末端的那个状态起个名字,叫分支名。(如果还没有提交过,则链末端就是第一个状态)
  4. 分支:所有曾经被叫过某个分支名的状态的组成的状态提交链称为一个分支。
  5. 创建一个新分支:给当前分支末端的状态再起一个名字。
  6. 切换到某分支:把现在的状态变为现在被称为某分支名的那个状态。
  7. (A分支与B分支)合并:把A分支名所对应的状态和B分支名所对应的状态中的文件整合在一起,作为一个新的状态。比如A分支状态有a,b,c三个文件,B分支状态有b,c,d三个文件(假设俩个b、c文件都相同),则合并后的状态有a,b,c,d 4个文件。
  8. 合并冲突:分支合并的过程中出现了不一致的情况,比如A状态中a文件某一行有”C=1″而B状态中a文件在同一行有”C=2″,这个时候需要手动修改a文件指定C到底等于几。
  9. 父分支:在A分支上创建了B分支,A分支称为B分支的父分支。
  10. 删除一个分支:仅仅将分支末尾的分支名取消掉,但保留已经提交的状态链。

术语的可视化理解见下图。

上图:状态2在A分支上,是现在的状态

上图:在(分支A上)提交一次

上图:回到最早的状态,这一次在分支A上创建了分支B

上图:切换到分支B,然后提交一次

上图:切换到分支A

上图:在分支A上提交一次

上图:在分支A上把分支B合并进来

上图:删除分支B

用途

此模型主要用于实现开发类型的任务

举例:进行新程序的开发/论文的写作/小组合作完成一件工作等

特点:在进行这类工作时我们主要的行为有

  1. 为工作不断增加新特性(新的程序功能/论文新的章节/工作新的任务)
  2. 当工作的状态相对稳定的时候,发布出一个阶段性成果(程序的一个发布版本/论文的n稿完成/工作的一个阶段完成)
  3. 发现并修改工作中的错误,进一步完善工作(程序bug的发现与debug/论文的修改/工作的完善)
  4. 经常需要进行大胆的实验,需要方便和安全的试错环境(debug中的尝试/论文中的新想法/工作中完成任务的新方法)

下图:开发型工作的开发流程示例 develop

分支类型

假设:存在以下几种类型的分支

  1. master类型分支,名为?|master或master,其中?为开发代号
  2. develop类型分支,名为?|develop或develop,其中?为开发代号
  3. feature类型分支,名为feature/*或?|feature/*或feature/*-FromBranch-?,其中*为特征描述,?为创建其的父分支
  4. release类型分支,名为release-*或?|release-*,其中*为要发布的版本号
  5. hotfix类型分支,名为hotfix-*或?|hotfix-*,其中*为要发布的版本号
  6. issues类型分支,名为issues/*或?|issues/*或issues/*-FromBranch-?,其中*为问题描述,?为创建其的父分支
  7. trials类型分支,名为?%trials.*,?为此分支的父分支,*为描述的名称(或直接为?%trials)

模型约束

下面介绍模型中的约定,并定义gg-*这样的抽象动作来完成约定中的行为

  • 每一次的提交都必须有意义:

git在每次提交的时候要求输入对此提交的概括,这个概括不能为空。

正确的提交概括:更新了程序doc

错误的提交概括:我刚才干了些啥?

  • 开发型任务中的master类型与develop类型分支必须成对出现,master分支的推进只能来源与release分支和hotfix分支的合并,禁止在master分支上直接提交。

注解:master分支上只有我们推送上去的稳定版本的程序,develop分支上的程序一直处于开发状态,不稳定。 在开发型任务中使用gg-init进行版本控制的初始化,建立配套的master~develop分支对。

  • feature类型分支满足:
  1. 只能从master,release类型之外的分支上创建
  2. 最终必须合并到创建他的父分支
  3. 最终分支被删除

注解:每当有新特性需要加入的时候,我们应该从develop类型分支上新建一个feature类型分支,完成新特性的开发和测试后将特性合并到develop类型分支上。

在develop类型分支上使用gg-feature-open featureName建立并转向一个名为feature/featureName的新分支

在一个feature类型分支上使用gg-feature-close把这个分支的工作合并到develop类型分支上,删除此分支,完成一个特性的开发

  • release类型分支满足:
  1. 只能从develop类型分支上创建
  2. 最终必须同时合并到master类型分支(发布新的版本)和develop类型分支(基于新版本的进一步开发)
  3. 最终分支被删除

注解:每当工作进入到一个较为稳定阶段的时候,我们可以使用gg-release-open versionNum建立并转向一个名为release-versionNum的临时分支,在这个分支上允许进行小的改动(比如修改一下readme文件中的版本号),然后使用gg-release-close将此版本合并(发布)到master类型分支上,同时合并到develop类型分支上,然后删除此分支。

  • hotfix类型分支满足:
  1. 只能从master类型分支上创建
  2. 最终必须同时合并到master类型分支(发布新的热补丁版本)和develop类型分支(基于新版本的进一步开发)
  3. 最终分支被删除

注解:当新版本发布后发现必须马上解决的严重bug时,我们应该使用gg-hotfix-open versionNum建立并转向一个名为hotfix-versionNum的临时分支,在这个分支上完成bug的修复,然后使用gg-hotfix-close将此版本合并(发布)到master类型分支上,同时合并到develop类型分支上,然后删除此分支。

  • issues类型分支满足:
  1. 只能从master,release类型之外的分支上创建
  2. 最终必须合并到创建他的父分支
  3. 最终分支被删除

注解:每当有(比较复杂的)问题需要解决的时候,我们应该从develop类型分支上新建一个issues类型分支,完成问题的调试后合并到develop类型分支上。

在develop类型分支上使用gg-issues-open featureName建立并转向一个名为issues/issuesName的新分支

在一个issues类型分支上使用gg-issues-close把这个分支的工作合并到develop类型分支上,然后删除此分支,解决了一个复杂的问题

issues类型和feature类型的实现方式一模一样,仅仅有名字上面的差别。

  • trials类型分支满足:

1.可以从除了master和release类型分支以外的任何类型分支上创建 2.在这个分支上请发挥想象力大胆实验

这个分支最后的结局可能为

1.接受实验结果,把实验过程并入父分支,称为good-close

2.实验结果不理想,放弃实验结果,从实验开始前重新来过,称为bad-close

3.最终分支被删除

注解:在满足条件的分支A上工作,时不时会冒出一些大胆的想法,这个时候使用gg-trials-open trialsName创建并转向一个名为A/trials.trialsName的实验分支,在这个分支上进行疯狂的实验,然后

  1. 使用gg-trials-good-close完成实验的和其父分支的合并,删除此分支或
  2. 使用gg-trials-bad-close进行状态指针的备份并删除此分支。

总结

模型提出了以下这些操作

gg-init

gg-feature-open

gg-feature-close

gg-release-open

gg-release-close

gg-hotfix-open

gg-hotfix-close

gg-issues-open

gg-issues-close

gg-trials-open

gg-trials-good-close

gg-trials-bad-close

这些操作的具体实现包括一些约束条件的检测(严格遵照上述模型才能提交)和一些git的操作,已经在https://github.com/Fmajor/gg中一一实现

gg 与 gitflow 的关系

gg与gitflow都是基于A successful Git branching model

gg比其多了trials branch和一些小工具(比如gg-comment)

gg可以认为是轻量级, 更符合博主个人习惯的gitflow, 个人用起来更顺手(学习成本更低)

博主的所有git仓库都使用gg进行管理