Git & Gitflow & Github

今天主要要分享的内容

  • Git 简史及常用用法
  • Gitflow 开发流程
  • Git 安装配置
  • GitHub 简介与业界实践介绍
  • Git 高级技巧

我看情况往下讲,讲到哪算哪

适合的听众

所有 IT 从业人员以及任何有写作以及协作写作需求的人士。

事实上 Git 的易用性使它已经不仅仅是一个『代码管理软件』,而成为了一个『分布式版本管理工作』,这是一个非常广泛的定义,能够覆盖到许多有文本写作需求的认识。

而现代人几乎离不开写作文案和协作的需求,所以,如果你希望提高你的效率的话,你几乎没有任何理由不学习 git。

最好的例子就是美国众议院使用 Github 来发起议案,德国联邦政府使用 Git 来撰写宪法。

Git 简史

众所周知以 linus torvalds 为首的开源团队一直维护着 Linux 这个庞大的开源项目。

虽然传说 linux 只使用 diffpatch 来管理代码😄,不过 Linux 开源组倒是从 2002 年起就使用 BitKeeper 来作为代码版本管理工具。

但是在 2005 年的时候,BitKeeper 终止了免费授权,开始转变为一个商业产品。迫于压力以及对于 svn 的厌恶,linus torvalds 最终决定亲手实现一个全新的版本管理工具。

也就有了我们今天所热爱的 git!

Git 的一些原理

长话短说,让我们来领略一下 git 的魅力

初始化

首先是初始化一个代码库

Git 的基本概念

首先我们需要理解 Git 的三个最重要的基本概念:

  • Working Directory(工作空间)
  • Git index(暂存区)
  • Git Directory(Git 库,已提交的历史)

add & commit

这里就可以提到 git 最常用的几个命令

当我们输入 git status 时,可以显示当前的状态,状态码为两位,第一位是 index 状态,第二位是 working directory 状态,具体为:

  • ??:未跟踪也未忽略文件
  • 🈳M:已跟踪,且有未 add 的修改
  • M🈳:修改已 add,尚未提交
  • MM:有已 add 的修改,但是在 add 后又有修改
  • A:新增文件(和 M 一样也有两位,省略)
  • D:删除的文件(和 A、M 一样也有两位,省略)

修改被提交到 index 后,我们就可以使用 commit 将其提交到 git directory 了

$ git commit 
$ git commit -m "<comment>"

如果你想要将当前工作区全部直接提交到 git 目录,也就是跳过 add,可以直接执行

$ git commit -am "<comment>"

Git Branch

有时候我们需要进行一些多人合作开发,或者是我们要同时开发好几个功能(feature),需要对同一个目录同时进行各不相同的更改,又不希望它们搅在一起难以区分,这时候我们就应该使用 git branch 了。

不同分之间的 git directory 是完全独立的,也就说在不同的分支上的提交也是互相隔离的。

但是需要注意的是,工作空间和 index 的修改是跟着 HEAD 走的,可以简单的理解为会随着你的 checkout 而同步移动的,具体分为两种情况:

  • 如果 checkout 的目的不与当前 index 和工作区冲突,会将其『携带过去』
  • 如果目的地会与未提交修改冲突,那么就会 checkout 不过去

举个例子,我们已经在 develop 分支里创建了一个 branch_demo.txt 文件,如果我们在 master 的工作区里也创建一个这个文件,再试图 checkout 到 develop,就会引发冲突:

据我观察,很多所谓『诡异的』『我怎么切不过去』大多都是这个原因

Git HEAD & Branch

既然讲到了分支,也提到了 checkout,继续深入讲下去前,我们需要回过头来深入了解一下 git 的分支机制。

不过先介绍一个好用的命令:git log

我们可以通过这个命令查询当前分支的所有历史生效提交

$ git log
$ git log --oneline --graph --decorate

如果你们注意的话,这已经是我第二次提到 HEAD 了,有人知道 HEAD 是什么吗?

git 的每一次提交,可以粗略的认为由几个指针组成,可以通过 $ git cat-file -p <commit_id> 来查看

可以看出每一个提交中都有一个指向 parent 的指针,一个提交链可以认为是一个链表,git 会持有分支的头部(比如 master、develop),这些头部根据 parent 指针递归的保持着整个提交链的引用。

如果对动态语言敏感的话,可能会注意到我用了 保持引用 这个用语,你也许会联想到 GC。

对的,git 也是有 gc 的,没有引用指向的幽灵提交是会被自动 gc 掉的,所以你在随心所欲的 checkout 和 reset 前最好三思。

这里就不深入展开了,之后有时间的话会讲到。

现在来回答之前的问题,HEAD 是什么。既然提到了链表,当然离不开头指针,git directory 里,各个分支头是头指针,HEAD 也是一种头指针,指向我们当前所在的 commit。

换个说法就是,HEAD 就是我们下次提交的 parent 所指向的提交。

理解了 HEAD 后再来看 checkout,就会发现 checkout 其实是移动 HEAD 的操作。

$ git checkout master

$ git checkout master^1  # master 之前的一次提交

趁机提一下幽灵分支

上一页里我们 checkout 到了 master 的前一个分支,这时 HEAD 指向提交 37c6。

如果这时候我们创建一个提交,这个提交的 parent 也会指向 37c6,而 HEAD 则会指向这个新提交。

但是需要注意的是,只有 HEAD 指向这个提交,一旦你这时 checkout 到其他地方,这个提交就会丢失引用,然后就会被 git gc,你也就丢失了这个分支。

如果你想要保留它的话,应该执行 $ git branch <new_branch_name> 建立一个分支头来指向这个提交,保持引用。

回到正题,说 checkout 只是用来移动 HEAD 的话有点肤浅,毕竟 checkout 也是身兼多职

$ git checkout .  # 将 HEAD 检出到当前工作空间
$ git checkout develop  # 切换 HEAD
$ git checkout <commit_id> <file_name>  # 检出指定提交的某个文件
$ git checkout -b <branch_name>  # 创建并跳转到分支,相当于 branch & checkout

OK,讲到这里你们应该已经理解了分支头、HEAD 和 checkout 的原理。还有不理解的吗?

疑问比较多的同学可能会注意到,既然可以用 checkout 移动 HEAD,那分支头可以移动吗?

以 git 的灵活度,只有你想不到,没有 git 做不到,答案是当然可以。我们可以用 reset

reset 的作用是把当前分支的 HEAD 和分支头移动到指定的 commit,移动方式又分为三种模式

  • --mixed:默认模式,移动后将跳过的提交放在工作区里
  • --soft:和 --mixed 类似,不过把跳过的提交放在 index 里
  • --hard:强行跳到指定提交,跳过的提交全部抛弃

使用 reset 移动分支头一般是因为以下几种情景:

  • 某个 feature 开发完毕,想把所有提交合并成一次提交再 merge,所以可以用 git reset --soft & git commit -am
  • 开发了一段时间后,想放弃最近一段时间的提交,可以直接 git reset --hard(非常不推荐,因为会引起线上仓库冲突!)
  • 想要找回某段历史,可以 git reflog & git reset(这个暂不展开讲了)

和 checkout 一样,reset 也不仅仅用来切换分支头,它的一个常用用法是用来重置 index。

当我们不小心 add 了一个错误的文件,想将其移出 index 区的话,可以简单的使用 git reset [<file_name>]

到这里,你已经知道了 git 的分支管理原理,以及 add, commit, checkout 和 reset 的用法,基本上已经可以一个人愉快的玩耍了

可能讲的比较快,不过我的主要目的是希望你们对原理有一定的理解,理解了原理后,再去学习具体的命令就会得心应手很多。

本地代码库与远程代码库

Gitflow 开发流程管理

不过学习 git 当然不是学来一个人玩,我们总会需要用 git 来解决多人合作的问题。

你以为用 git 合作开发就是一群人在一起 add & commit & push ?

多人合作开发意味着我们需要解决多人开发时的冲突、不同功能开发隔离、线上 bug 修复、按版本发布等等等等诸多问题…

如果是更多人的大公司的话,还需要解决权限、代码 review 等等问题…

有没有很头大?

不过不用怕,早有了 gitflow 来拯救我们,配合良好的软件工程和编码习惯,完全可以搞定大型项目的开发流程管理

在 gitflow 里,branch 是核心。gitflow 依赖于众多的 branch 对开发流程进行高度分工,并依赖于一套 branch 合并规则来组织软件的开发流程。

有经验的 git 使用者一般都会使用两个分支:

  • master:正式分支,也是线上服务器在运行的版本;
  • develop:开发分支,众多开发人员直接提交的分支。

gitflow 也有这两个分支,不过它引入了更多…

在 gitflow 里,一共有五种类别的分支:

  • master:唯一的主分支;
  • develop:唯一的开发分支,用于收集诸多的开发代码;
  • feature:功能分支,数量任意,用于开发单一功能;
  • hotfix:紧急修复分支,原则上也仅有一个,开启后应尽快修复;
  • release:发布分支,每一个待发布的版本对应一个分支。

下面我按照使用情景在介绍 gitflow 的具体流程。

初始化

初始化很简单,就是首先我们要在原始 master 的基础上,新建一个 develop 分支。

功能开发

假设这时候我们开了一个项目需求会,项目经历确认了有 10 个 需求需要开发,然后我领到了其中的 3 号 需求。

这时候我应该从当前的开发分支中切出一个功能分支,进行开发工作

功能分支的命名规则是: feature/<issue_num>-<feature_name>

然后我们就在 feature/3-title 分支上进行开发工作,每次的提交信息建议都符合一定的格式,比如推荐使用 commitizen 规范:

fix(data-3): <short summary>

<long description>

比如这里我就执行: git ci -am 'fix(data-3): typo error'

(稍后我再介绍下 commitizen 规范)

功能开发完成后,经过完善测试,如果需要,我们就可以将功能分支合并到 develop 分支。

合并也是一门哲学,方法多种多样,这里我们就简单点:

使用 --no-ff 是为了不污染主干分支,保留一个清晰可见的 merge commit

更严格的说:

  • 每一个 merge 都应该有一个显式的 merge commit
  • 每次 merge 只能有一个 commit

万一合并的时候遇到冲突,我们应该立刻解决它

解决完冲突后应该对合并结果进行提交,并删除 *.orig 临时文件

在更为严格的模式下,你应该先解决了冲突,再 merge 进主干。或者这么说,你应该在合并后的代码上做测试,测试完成后才能 merge 进主干。

强烈建议在图形化界面下解决冲突!

pull & push

补充一点,在开发的过程中,我们可能希望将开发中的分支也在远程代码库保存一份

# 如果是新建的分支的话,首先需要在远程创建对应的分支
$ git push --set-upstream origin feature/1-title

# 然后就可以像往常一样的 push & pull 了
$ git pull
$ git push

如果我们已经不再需要这个分支,希望将它从本地和远程代码库里删除的话,可以用:

# 删除本地分支
$ git branch -d <branch_name>

# 删除远程分支
$ git push origin :<branch_name>

发布版本

经过一段时间的开发,我们已经在 develop 上积累了足够做一次版本发布的新功能,应该开始走版本发布的流程了。

在 gitflow 中与之对应的是 release 分支,我们可以在 develop 上建议一个对应相应版本号的发布分支

一句话搞定

不过需要注意的是,release 分支一但建立,就代表着某一版本的代码基本固定,之后不应该在 release 上提交任何功能性的代码,而只应该进行必须的 bugfix

在环境上,release 对应的是 pre 环境,也就是最终上线前的最后一次测试。如果有 bug,可以直接在 release 上进行提交。

release 分支经过测试没问题之后,就可以合并了。

release 的合并策略比较特殊,是要 分别合并到 master 和 develop

# 合并到 develop
$ git checkout develop
$ git merge --no-ff release/1.0
$ git pull
$ git push

# 合并到 master
$ git checkout master
$ git merge --no-ff release/1.0
# 修改版本信息等等线上配置更新。。。
$ git commit -am '1.0'
$ git push  # 正式上线

紧急修复

除了日常的功能开发和版本发布外,有时候我们会发现线上版本有 bug 需要立刻修复,gitflow 提供了 hotfix 来完成这一功能。

我们需要从 master 上 checkout 出一个 hotfix 分支,然后分别合并到 master 和 develop 上。

gitflow 的核心内容差不多就这些了,再重温一下这张流程图

为了更好的协作,gitflow 也有一些成文或不成文的约定:

  • master 的每一次更新,最好都伴随着版本号的变化
  • release 一旦创建,除了 bugfix 外不要有功能性的提交
  • 每一个功能需求都要仔细拆分,落实到每一个 feature 上
  • master 和 develop 都必须是稳定的主干分支,所有的 bug 都应该立即修复
  • develop 上不应该有冲突代码,要么你在合并的时候立刻搞定,要么你可以切一个 work 分支来解决完冲突后再合并回主干

所以,gitflow 的推行,其实在一定程度上也依赖于项目的良好拆分,最好能把 issue 拆分到只对应一个子模块的功能,issue 与 issue 之间最好不要出现重叠。

具体的细节,还需要在开发过程中进行磨合。

推荐一个叫做 SourceTree 的工具,上面提到的这些操作都可以在 GUI 上点点鼠标就简单实现

Commitizen

项目地址:https://github.com/commitizen/cz-cli

Commitizen 是一种针对 git commit comment 格式的约定,具体来讲,将 git commit 区分为了几种格式:

null

这些提交,为后续的自动化版本管理,提供了基础。

git 安装配置

我们现在大量使用 CentOS6,上面的 git 版本还是 1.7,现在的最新 git 版本已经到了 2.5.3,为了使用新特性,建议升级到最新版本的 git

# 安装依赖
$ sudo yum install -y curl-devel expat-devel gettext-devel openssl-devel zlib-devel
$ sudo yum install -y gcc perl-ExtUtils-MakeMaker

# 卸载老 git
$ sudo yum remove -y git

# 下载、安装新版 git
$ wget https://www.kernel.org/pub/software/scm/git/git-2.5.0.tar.gz
$ tar xf git-2.5.0.tar.gz
$ cd git-2.5.0
$ sudo make prefix=/usr/local/git all
$ sudo make prefix=/usr/local/git install
$ sudo echo "export PATH=$PATH:/usr/local/git/bin" >> /etc/bashrc
$ source /etc/bashrc

# 检查一下版本
$ git --version
git version 2.5.0

git 配置

我们可以用命令行配置

# 配置用户
$ git config --global user.name "your_username"
$ git config --global user.email your_email_address

# 设置 git 颜色显示
$ git config --global color.ui true

也可以修改 git 的配置文件。git 有两个生效的配置文件

  • 全局配置文件 ~/.gitconfig
  • 当前项目的配置 .git/config

在全局配置里,我们也可以配置用户、合并算法和 alias 等等

[user]
    name = Laisky
    email = [email protected]
[color]
    ui = true
[alias]
    st = status -s
    sto = status -uno
    di = diff --patience
    dic = diff --patience --cached
    co = checkout
    ci = commit
    br = branch
    mg = merge
    sta = stash
    ps = push
    pl = pull
    dlog = log --decorate
[diff]
    algorithm = patience
[push]
    default = simple
[core]
    excludesfile = /Users/laisky/.gitignore_global
    editor = /usr/local/bin/vim

Github 实践

如果要使用 git 协作,那就离不开远程仓库。

当然理论上我们可以使用任何一台支持 SSH 的主机当做远程仓库,不过既然 github/gitlab 是这么的普及,还是值得介绍一下。

注册登录、公私钥配置这种废话我就不讲了

我就从业界实践的角度出发,来讲一下我们可以怎么去利用 github

  • issue
  • milestone
  • code review
  • wiki
  • webhooks
  • deploy keys
  • pull request
  • gist
  • git page

issue & milestone & label

github 的 issue 是一个非常好用的功能,很多人只是简单的把它当做 Q&A,这其实是极大的浪费,因为 Github 本身就是一个强大的项目管理工具。

举一个实践的例子,当产品经理确认了项目需求后,项目经理应该将其该需求确认工期,并将其拆分为数个功能单一的 feature。

对应在 github 上,我们应该为这个版本建立一个 milestone(如果需要的话还可以指定截止日期),然后再为每一个 feature 创建一个 issue。

创建 issue 时可以指定 label 和 assignee

简而言之,我们应该为每一个大的版本创建一个 milestone,为每一个 feature 创建一个 issue。

对于各种不同性质的 issue,比如 Q&A、feature、bug、调研等等,我们可以通过 label 来区分

而且因为 github 会自动为 issue 创建一个唯一的 issue_id,所以我们之前提到在给 feature branch 命名的时候,都是 feature/1-title 格式

而且在每次提交的时候,都带上所述的 issue 号,比如

$ git ci -am '* #1 fix typo'

这样的好处是,github 会自动提取信息中的 #1,然后在所指向的 issue 中显示该提交

这样做的好处是,我们可以迅速找到为了解决某 issue 的全部提交代码。养成这个好习惯可能会在很多场合中挽救你。

code review

当某人完成了 feature 的开发后,你可以要求他必须在你 review 完成后再合并到主干。

review 的方法也很简单,你可以点击任意提交,进入提交详情页,看到所有的代码修改细节,并且如果有疑问的话,可以在任意一行留下评论,要求对方解答

wiki

github 还提供了 wiki 页,可以放置任意页数的文档

文档支持 markdown 格式,而且每一页发布的文档都提供固定的链接,我们可以在第一页上组织起文档的目录结构

这个比较简单,不多介绍

webhooks

其实如果你用得好的话,git & github 本身就是一套很好用的自动集成、部署工作。

在 github 项目的 settings 页面里,有一项 webhooks & service,你可以从多达二十多个的 github 事件中(比如 push、pull 等),选定需要监听的事件。

当事件发生时,github 会向你指定的 URL 发起一个 POST 请求,你可以监听这个 POST 请求来触发任何你需要的工作

这个 POST 中会携带项目参数以及 signature,以表明身份,不用担心接口被滥用

详情可以参看文档 https://developer.github.com/webhooks/#ping-event

用好 webhooks 可以做很多很多的事情,比如

  • 谷歌就使用了大量监听 webhooks 的机器人来协助完成开源项目。
  • 利用 travis-ci 完成集成测试
  • 利用 webhooks 促发自动部署

deploy keys

想象一个场景,我们有一个私有的项目仓库,然后打算在服务器上直接从这个仓库 pull。

因为项目是私有的,所以服务器必须需要有权限。有几种方法可以实现

  • 将服务器的私钥添加进你的账户

这样 github 会认为服务器就是你本人在操作。万一你的同事在服务器上干了很多『坏事』,在 git 的日志上却会显示着你的名字

  • 将服务器私钥作为一个独立用户,添加进项目

服务器将会拥有完全的权限,包括 push,这显然是不必要的,而且也是潜在风险

  • deploy key

我们可以将服务器的公钥作为 deploy key 添加进 github,然后在相应的项目仓库里绑定这个 deploy key。这样服务器就对该项目有了只读权限

pull request

参加过开源项目的同学应该对这个很熟悉。pull request 用来对一个没有权限的项目发起提交申请。

方法是进入任意一个项目主页,点击右上方的 Fork,就会在你自己的账户下简历一个副本。

然后你就在这个副本项目里,像正常一样的做出修改,然后提交。

接下来是重点,提交完成后,你要向原始项目发出合并请求(pull request)了

点击进入原始项目的页面,选择 pull request tab 页,然后点击右上角的 New pull request

接下来就是填写必要的说明,选择需要合并的提交,点击 create pull request 即可。然后就是等待项目所有者的审查,如果顺利的话,对方就会将你的提交 merge 到原始项目之中。

经常听到有人抱怨说 git 缺乏权限控制,其实 pull request 就是 git 的权限控制

新人进来后,可以不给他项目仓库的写权限,他要提交代码都必须通过 fork & pull request 的形式提交,由 mentor review 之后,才合并到项目库中。

对于一些比较大型的项目,可以将其拆分为数个子项目,每个项目建立一个仓库,然后将人员按照权限分配到各个仓库之中。

每个人只能看到并开发自己的那部分代码,而拥有最高权限的人可以通过 submodule 或 subtree 将各个子模块组织起来

下面介绍的三个东西在我们公司内使用的 gitlab 里没有,不过我也稍微提一下

github 不仅仅是一个代码托管网站,其实还提供了许多其他的功能。

gist

官方地址 https://gist.github.com

这是一个 github 提供的分享『小代码片段』的页面

gitpage

官方地址 https://pages.github.com

github 其实不仅仅是一个代码托管地址,还给你提供了一个网页展示的功能。

gitter

官方地址 https://gitter.im

这是一个基于 repo 的聊天室,方便大家对项目进行讨论

除此之外,github 还有很多功能值得你去探索

Markdown

简单的插入,提一下 Markdown

markdown 是一种排版格式,以这种格式编写的文件会使用 .md 后缀。

markdown 是为了取代繁琐的 reStructuredText 格式而出现的。

而 github 使用的是一种更简化版的 github-flavor markdow

其实用起来很简单,你花二十分钟看一下这两篇就足够了:

我这篇 slides 就是用 markdown 写的

Git 工程实践

Github 就算介绍完了,下面我们来介绍一些在实际应用中的工程实践

偷懒的话可以去读 ruanyifeng 的一系列文章

Git 并不简单,用好 git 需要记住大量的命令,阅读大量的文档、资料以及大量的实践。

Git 高级技巧

其实也不算高级技巧,就是一些不那么常用到的小技巧😄

  • stash
  • diff
  • blame
  • tag
  • revert
  • cherrypick
  • githooks
  • reflog
  • rebase

我也随意讲了,讲到哪算哪

stash

  • $ git stash: 可以将当前 index 和 working directory 的所有未提交内容压入 stash 栈中
  • $ git stash pop: 将栈顶存储的内容恢复到工作空间
  • $ git stash list: 可以显示 stash 栈的所有内容
  • $ git stash clear: 可以清空栈

diff

  • $ git diff: 查看当前工作区和 index 去的区别,也就是尚未 add 的修改
  • $ git diff head:查看当前 index 和工作区与 head 的区别,也就是查看所有尚未 commit 的修改
  • $ git diff <commit> <commit>:查看任意两个 commit 间的区别

blame

  • $ git blame <file>:可以查看某一个文件的每一行具体是谁在什么时候提交的

tag

git 可以通过 tag 来做版本管理,在 github 上会自动将 tag 打包成 tar 包并提供下载链接

  • $ git tag <tagname> -m <message>:将当前 head 所在的提交标记为 tag
  • $ git tag <tagname> <commit> -m <message>:将某个指定提交标记为 tag
  • $ git tag -n2: 查看 tag,可以指定为 -n1 或 -n2
  • $ git tag -d <tagname>:删除 tag
  • $ git push origin --tags:推送 tags 到远程仓库
  • $ git pull origin --tags:从远程仓库下拉 tags
  • $ git push origin :refs/tags/<tag_name>:删除远程的 tag

revert

用于反向提交。当你要撤销掉某次提交的时候,切记不要用 reset(因为可能会引起冲突),而要用 revert 进行一次反向提交,抵消到该提交的所有修改。

  • $ git revert <commit>

cherry pick

可以提取某个指定的提交,合并到 HEAD 处再提交一次。

和 merge 的区别是,cherry pick 只针对某一个指定的提交,而不会将其上下游都带过来。

  • $ git cherry-pick <commit>

githooks

githooks 的功能类似于之前提到过的 github 的 webhooks,不过 githooks 是作用在本地的,功能也是监听各种 git 事件,然后调用脚本。

你可以在服务器上配置 githooks 监听 post-receive,当服务器接收到新代码后,就自动重启应用玩成更新。

githooks 的脚本都在 .git/hooks/

# vi .git/hooks/post-receive

#!/bin/sh
unset $(git rev-parse --local-env-vars)

cd /home/laisky/repo/laisky-blog/
git stash
git pull

# script
# do what you want