目录

Git Workflow

工作流 - Workflow:是对工作流程及其各操作步骤之间业务规则的抽象、概括描述。工作流建模,即将工作流程中的工作如何前后组织在一起的逻辑和规则,在计算机中以恰当的模型表达并对其实施计算。工作流要解决的主要问题是:为实现某个业务目标,利用计算机在多个参与者之间按某种预定规则自动传递文档、信息或者任务。

Git Workflow

为了让大家有效地合作,使得项目井井有条地发展下去而抽象出来的一系列 git 操作步骤及流程,没错,是针对 git 的操作使用流程。

常见的工作流

集中式工作流

集中式工作流 - Centralized Workflow

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1006-centralized-workflow.svg

创建中央仓库

假定 git 创建了一个裸仓库:

1
2
3
4
5
$ ssh git@host
$ cd ~ && mkdir -pv repository

// 创建一个裸仓库,按照约定,加上 .git 扩展名
$ git init --bare /home/git/repository/repo.git

开发人员介入

第一阶段:本地开发
技巧
这一阶段,qwert 可以按自己需求反复操作多次,而不用担心中央仓库上有了什么操作,同样的情况也适用于 poiuy

开发者 qwert

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 拉取代码
$ cd ~ && git clone ssh://git@host/home/git/repository/repo.git

// 查看状态
$ git status

// 创建 README.md 文件
$ touch README.md

// 将 README.md 文件加入暂存区
$ git add README.md

// 设置用户名和邮箱
$ git config user.email "qwert@example.com"
$ git config user.name "qwert"

// 提交代码到本地仓库
$ git commit -m "Add README.md"

开发者 poiuy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 拉取代码
$ cd ~ && git clone ssh://git@host/home/git/repository/repo.git

// 查看状态
$ git status

// 创建 README.md 文件
$ touch README.md

// 将 README.md 文件加入暂存区
$ git add README.md
$ vim README.md
1. this is the first commit.

// 设置用户名和邮箱
$ git config user.email "poiuy@example.com"
$ git config user.name "poiuy"

// 提交代码到本地仓库
$ git commit -m "Add README.md"

直到这时两个人都相安无事,在各自的本地仓库玩的尽心。但事情不会就这样了结了,否则,我就没法往下写了。

第二阶段:远程同步

这时起,情况就变的有些复杂了,还好局面可控。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// qwert 提交代码到远程仓库
$ git push origin master

// 接下来,poiuy 也将代码提交到远程仓库,这时真正的悲剧产生了
$ git push origin master
To ssh://git@host/home/git/repository/repo.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'ssh://git@host/home/git/repository/repo.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 merge the remote changes (e.g.,
hint: 'git pull') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

// 也就是说,poiuy 得先拉一下远程仓库的代码到本地,再进行提交,因为你在人家 push 后面进行 push 的,这不怪你
$ git pull --rebase origin master
warning: no common commits
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ssh://host/home/git/repository/repo
 * branch            master     -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: Add README.md
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging README.md
警告
--rebase:这个选项就是告诉 Gitpoiuy 的提交移到同步了中央仓库修改后的 master 分支的顶部。如果你忘加了这个选项,pull 操作仍然可以完成,但每次 pull 操作要同步中央仓库中别人修改时,提交历史会以一个多余的『合并提交』结尾。对于集中式工作流,最好是使用 rebase 而不是生成一个合并提交。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// qwert 续续编辑文件 README.md 并提交推送到远程
$ vim README.md
1. this is the second commit. # 只将单词 first 改为 second
$ git add README.md
$ git commit -m "Update README.md"
$ git push origin master

// poiuy 使用 git pull 同步远程代码到本地,这将产生一个冲突
$ git pull origin master
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ssh://host/home/git/repository/repo
 * [new branch]      master     -> origin/master
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

// poiuy 使用 git pull --rebase 同步远程代码到本地,托 --rebase 的福没有产生冲突
$ git pull --rebase origin master
From ssh://host/home/git/repository/repo
 * branch            master     -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Fast-forwarded master to 3a24fcf204956f97a7499393fbca68e3a77df040.

// poiuy 继续编辑 README.md
$ vim README.md
1. this is the second commit.
2. poiuy # 新增一行
$ git add .
$ git commit -m "Update README.md"
$ git push origin master

// qwert 继续编辑 README.md
$ vim README.md
1. this is the second commit.
2. qwert # 新增一行
$ git add .
$ git commit -m "Update README.md"
$ git push origin master # 这时远程仓库拒绝 qwert 的提交,因为 poiuy 已经将最新改动提交到远程了,qwert 必须将远程同步到本地
To ssh://git@host/home/git/repository/repo.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'ssh://git@host/home/git/repository/repo.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 merge the remote changes (e.g.,
hint: 'git pull') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git pull --rebase origin master # 不是说拉取的姿势不对,而是两者都对同一行内容进行编辑,冲突无疑呀
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ssh://host/home/git/repository/repo
 * branch            master     -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: Update README.md
Using index info to reconstruct a base tree...
M  README.md
Falling back to patching base and 3-way merge...
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Failed to merge in the changes.
Patch failed at 0001 Update README.md
The copy of the patch that failed is found in:
   /home/qwert/repo/.git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

// qwert 解决冲突,解决一个冲突
$ git add <conflict-to-resolve-file>
$ git rebase --continue # 直到出现 No rebase in progress?

// qwert 在解决冲突的过程中,发现有些搞不定,弄乱了,还可以撤销
$ git rebase --abort

// 如果 qwert 顺利解决了所有冲突,就愉快地发布他的修改
$ git push origin master

正如引用文章中提到的,仅使用几个 Git 命令我们就可以模拟出传统 Subversion 开发环境。对于要从 SVN 迁移过来的团队来说这太好了,但没有发挥出 Git 分布式本质的优势。

集中式工作流的使用场景:

  • 团队人数少
  • 开发不是很频繁
  • 团队沟通方便
  • 不需同时维护多个版本

Gitflow 工作流

Gitflow 工作流 - Gitflow Workflow

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1007-gitflow-workflow.png

拉取远程仓库

Gitflow 工作流仍然用中央仓库作为所有开发者的交互中心。这里我沿用集中式工作流中创建的仓库。

1
2
3
4
5
6
7
8
9
// qwert 工作准备
$ ssh qwert@host && cd ~ && git clone ssh://git@host/home/git/repository/repo.git
$ git config user.name "qwert"
$ git config user.email "qwert@example.com"

// poiuy 工作准备
$ ssh poiuy@host && cd ~ && git clone ssh://git@host/home/git/repository/repo.git
$ git config user.name "poiuy"
$ git config user.email "poiuy@example.com"

Gitflow 工具安装

Gitflow 两个长期分支:

  • 主分支 master
  • 开发分支 develop

Gitflow 三个短期分支:

  • 功能分支 feature branch
  • 补丁分支 hotfix branch
  • 预发分支 release branch

关于 git-flow 这篇 Learn Version Control with Git 说的非常到位。

1
2
3
4
// Linux 下安装
$ curl --insecure --silent --location  https://raw.githubusercontent.com/petervanderdoes/gitflow-avh/develop/contrib/gitflow-installer.sh --output ./gitflow-installer.sh
$ chmod +x ./gitflow-installer.sh
$ sudo ./gitflow-installer.sh install stable

Gitflow 初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// qwert 用户初始化 git-flow
$ git flow init

Which branch should be used for bringing forth production releases?
   - master
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Bugfix branches? [bugfix/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []
Hooks and filters directory? [/home/qwert/repo/.git/hooks]
$ git push -u origin develop # 将 develop 分支提交到远程仓库

// poiuy 用户初始化 git-flow
$ git flow init

Which branch should be used for bringing forth production releases?
   - master
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Bugfix branches? [bugfix/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []
Hooks and filters directory? [/home/poiuy/repo/.git/hooks]

开发阶段

开发者 qwert

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// qwert 开启一个功能分支,进行项目某个功能开发
$ git flow feature start feature-qwert
Switched to a new branch 'feature/feature-qwert'

Summary of actions:
- A new branch 'feature/feature-qwert' was created, based on 'develop'
- You are now on branch 'feature/feature-qwert'

Now, start committing on your feature. When done, use:

     git flow feature finish feature-qwert

// qwert 操作
$ vim README.md
1. this is the second commit.
2. poiuy
3. qwert
4. qwert feature start.
$ git add README.md
$ git commit -m "Update README.md"
$ git flow feature finish feature-qwert # 此时 qwert 将完成开发结束任务,feature-qwert 分支的使命就此完结
Switched to branch 'develop'
Updating d62035e..ced573c
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)
Deleted branch feature/feature-qwert (was ced573c).

Summary of actions:
- The feature branch 'feature/feature-qwert' was merged into 'develop'
- Feature branch 'feature/feature-qwert' has been locally deleted
- You are now on branch 'develop'
$ git push origin develop # 将 develop 分支提交到远程仓库

开发者 poiuy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// poiuy 开启一个功能分支,进行项目另一个功能开发
$ git flow feature start feature-poiuy
Switched to a new branch 'feature/feature-poiuy'

Summary of actions:
- A new branch 'feature/feature-poiuy' was created, based on 'develop'
- You are now on branch 'feature/feature-poiuy'

Now, start committing on your feature. When done, use:

     git flow feature finish feature-poiuy

// poiuy 操作
$ vim README.md
1. this is the second commit.
2. poiuy
3. qwert
4. poiuy feature start.
$ git README.md
$ git commit -m "Update README.md"
$ git flow feature finish feature-poiuy # 此时 poiuy 将完成开发结束任务,feature-poiuy 分支的使命就此完结

// poiuy 跟踪远程分支
$ git branch --set-upstream-to=origin/develop develop
Branch develop set up to track remote branch develop from origin.
$ git push origin develop # poiuy 将开发好的功能推送到远程,但抱歉的时,此时远程 develop 已经向前滚动,因为不只是你一个人在开发
To ssh://git@host/home/git/repository/repo.git
 ! [rejected]        develop -> develop (non-fast-forward)
error: failed to push some refs to 'ssh://git@host/home/git/repository/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git pull origin develop # 那就先拉取一下,这时如果和其它人对同一块内容做过改动,冲突是没有悬念的
From ssh://host/home/git/repository/repo
 * branch            develop    -> FETCH_HEAD
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
$ git push origin develop # poiuy 只能吭哧吭哧解决完冲突,再推送到远程

这时,开发者 qwert 又开发完一个新的功能,准备要发布

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// 在开启新的功能分支之前,先从 develop 分支拉取最新的代码
$ git pull origin develop

// 开启新的功能分支
$ git flow feature start feature-something
$ vim README.md
1. this is the second commit.
2. poiuy
3. qwert
4. qwert feature start.
5. poiuy feature start.
6. qwert another feature start.
$ git add README.md
$ git commit -m "Update README.md"

// 将开发完的新功能分支发布到远程仓库,这个分支可以发布到测试或者仿真环境进行测试
$ git flow feature publish feature-something
Counting objects: 5, done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 303 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To ssh://git@host/home/git/repository/repo.git
 * [new branch]      feature/feature-something -> feature/feature-something
Branch feature/feature-something set up to track remote branch feature/feature-something from origin.
Already on 'feature/feature-something'

Summary of actions:
- The remote branch 'feature/feature-something' was created or updated
- The local branch 'feature/feature-something' was configured to track the remote branch
- You are now on branch 'feature/feature-something'

// 如果这个功能有多个人员参与,那么其它人可以拉取这个新功能分支进行相关的开发
$ git flow feature pull origin feature-something

// 也可以跟踪在 origin 上的新功能分支
$ git flow feature track feature-something

// 经过测试发现这个新功能分支没什么大的问题,开发者将完成的新功能合并到 develop 分支并删除新功能分支
$ git flow feature finish feature-something

// 开始从 develop 准备 release 版本
$ git flow release start v1.0

// 创建之后可以立即发布允许其它用户向这个 release 分支提交内容,将这个 release 发布出去,允许更新提交,确保新功能正常,万一新功能有隐患的 bug,可以就近修复
$ git flow release publish v1.0

// poiuy 可以通过 track 命令签出 release 版本的远程变更
$ git flow release track v1.0
$ vim README.md # poiuy 跟踪 release v1.0 版本后,又进行了一些提交
1. this is the second commit.
2. poiuy
3. qwert
4. qwert feature start.
5. poiuy feature start.
6. qwert another feature start.
7. poiuy tract release v1.0

// 经过灰度的测试后,发布的 release 符合上线的要求,qwert 拉取 release v1.0 的更新,完成 release 版本。至此新功能才能算是上线了
$ git flow release finish v1.0
Branches 'develop' and 'origin/develop' have diverged.
And local branch 'develop' is ahead of 'origin/develop'.
Switched to branch 'master'
Merge made by the 'recursive' strategy.
 README.md | 4 ++++
 1 file changed, 4 insertions(+)
Already on 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)
Switched to branch 'develop'
Your branch is ahead of 'origin/develop' by 1 commit.
  (use "git push" to publish your local commits)
Merge made by the 'recursive' strategy.
 README.md | 1 +
 1 file changed, 1 insertion(+)
To ssh://git@host/home/git/repository/repo.git
 - [deleted]         release/v1.0
Deleted branch release/v1.0 (was 77da54a).

Summary of actions:
- Release branch 'release/v1.0' has been merged into 'master'
- The release was tagged 'v1.0'
- Release tag 'v1.0' has been back-merged into 'develop'
- Release branch 'release/v1.0' has been locally deleted; it has been remotely deleted from 'origin'
- You are now on branch 'develop'

// 将 tag 推送到远程仓库
$ git push origin --tags

// 没过多久,生产环境出现了一个致命的 bug 需要紧急修复,qwert 开启一个修复分支
$ git flow hotfix start issue-1
Branches 'master' and 'origin/master' have diverged.
And local branch 'master' is ahead of 'origin/master'.
Switched to a new branch 'hotfix/issue-1'

Summary of actions:
- A new branch 'hotfix/issue-1' was created, based on 'master'
- You are now on branch 'hotfix/issue-1'

Follow-up actions:
- Start committing your hot fixes
- Bump the version number now!
- When done, run:

     git flow hotfix finish 'issue-1'

// qwert 开始修复 bug,完成后结束 hotfix 分支
$ vim README.md
1. this is the second commit.
2. poiuy
3. qwert
4. qwert feature start.
5. poiuy feature start.
6. qwert another feature start.
7. poiuy tract release v1.0
8. qwert hotfix start.
$ git add README.md
$ git commit -m "Update README.md"
$ git flow hotfix finish 'issue-1' # 结束 hotfix 分支的 bug 修复
Branches 'master' and 'origin/master' have diverged.
And local branch 'master' is ahead of 'origin/master'.
Branches 'develop' and 'origin/develop' have diverged.
And local branch 'develop' is ahead of 'origin/develop'.
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)
Merge made by the 'recursive' strategy.
 README.md | 1 +
 1 file changed, 1 insertion(+)
Switched to branch 'develop'
Your branch is ahead of 'origin/develop' by 3 commits.
  (use "git push" to publish your local commits)
Merge made by the 'recursive' strategy.
 README.md | 1 +
 1 file changed, 1 insertion(+)
Deleted branch hotfix/issue-1 (was e4ac4c3).

Summary of actions:
- Hotfix branch 'hotfix/issue-1' has been merged into 'master'
- The hotfix was tagged 'issue-1'
- Hotfix tag 'issue-1' has been back-merged into 'develop'
- Hotfix branch 'hotfix/issue-1' has been locally deleted
- You are now on branch 'develop'

// 提交修复的代码到远程 develop 分支
$ git push origin develop

// 将修复后的 tag 提交到远程仓库,这样生产环境就可以拉取 issue-1 标签修复之前的 bug
$ git push origin --tags

以下的这副图就是对 git flow 重要命令的诠释:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1008-gitflow-commands.png

通过这些操作,我们看到 Gitflow 工作流自始至终一直维护 masterdevelop 分支,而 featurehotfix 分支只是短暂的出现,待功能完成后合并到 develop 分支,修复完成后,同时合并到 masterdevelop 分支。同时我们也看到新功能提交从不直接与 master 分支交互,所有的功能分支都基于 develop 分支,当 feature 达到上线的要求后,才结束 feature,并将 feature 分支合并回 masterdevelop 分支,释放出一个版本供生产发布。这样明晰的功能分支提供了一个健壮的用于管理大型项目的框架。当然,要长期维护两个分支也比较麻烦,大多数管理工具都将 master 当作默认分支,可是开发是在 develop 分支进行的,这导致经常要切换分支,稍不注意就弄错分支,非常恼人,也说明了这种工作流程终究是不合适小团队的中小型项目。

Gitflow 工作流的使用场景:

  • 认为额外学习 Gitflow 不是什么问题的
  • 有专门的代码仓库管理员的
  • 开发团队相对固定,而且有一定规模
  • 常常有并行开发需求
  • 团队对于 ReleaseBugFeature 这些概念有统一定义标准的

Forking 工作流

Forking Workflow

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1009-git-forking-workflow.png

为了演示 Forking Workflow 的工作流程,我准备了三个 github 账号,假设为维护者 git、开发者 qwertpoiuy

创建远程仓库

维护者 git 创建一个公开的远程仓库:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1010-git-forking-create.jpg

开发者 qwertFork 维护者 git 创建的项目,当然,开发者 poiuy 也和 qwert 一样去 Fork 仓库:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1011-git-forking-repo.jpg

完成之后,qwertpoiuy 相当于把远程 git 创建的项目克隆到各自的远程仓库

功能开发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// qwert 拉取远程仓库代码到本地
$ git clone https://github.com/qwert/repo.git
$ git config user.name "qwert"
$ git config user.email "qwert@example.com"
$ vim README.md
# repo
This is a forking workflow\'s demo.

## Develop Feature

1. this is written by Quert.
$ git add README.md
$ git commit -m "Update README.md"
$ git push -u origin master

// poiuy 拉取远程仓库代码到本地
$ git clone https://github.com/poiuy/repo.git
$ git config user.name "poiuy"
$ git config user.email "poiuy@example.com"
$ vim README.md
# repo
This is a forking workflow\'s demo.

## Develop Feature

1. this is written by Poiuy.
$ git add README.md
$ git commit -m "Update README.md"
$ git push -u origin master

发起 Pull Request

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1012-git-forking-pr-create.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1013-git-forking-pr-open.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1014-git-forking-pr-show.jpg

可以看到开发者 qwert 没有权限合并代码,此时只有维护者 git 才能合并代码,维护者 git 去合并代码:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1015-git-forking-merged.jpg

开发者 poiuy 也采取和 qwert 一样的流程,但是当他申请合并时,出现了冲突,这是预科中的事,因为两人同时对一个文件相同的区域进行了编辑:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1016-git-forking-conflicts.jpg

poiuy 去解决冲突:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1017-git-forking-resolved-conflicts.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1018-git-forking-mark-resolved.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1019-git-forking-resolved-conflicts-merged.jpg

可以看到开发者 poiuy 也没有权限合并代码,此时也只有维护者 git 才能合并代码:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1020-forking-feature-merged.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1021-forking-close-merged.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1022-forking-feature.jpg

至此,qwertpoiuy 开发的新功能都合并到远程维护者的 master 分支,这一阶段的开发告一段落。

Forking 工作流的使用场景:

  • 常用于开源软件
  • 开发者有需要衍生出自己的衍生版的
  • 开发者不固定,可能是任意一个远程工作者

Feature branching 工作流

Feature Branching Workflow

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1023-feature-branch-workflow.png

这个流程大概算是 Gitflow 的简化版,不再需要安装额外 Git 插件,这个流程配合一些代码托管平台可以玩的风生水起。

特性分支下拉取远程仓库

功能分支工作流中我们仍然使用之前创建的中央仓库,并且 master 分支还是代表了正式项目的历史。这种工作流不是直接提交本地历史到各自的本地 master 分支,开发者每次在开始新功能前先基于 master 创建一个新分支,给分支起一个描述性的名称,这样分支用途就比较明显。

准备工作还是老套路

1
2
3
4
5
6
7
8
9
// qwert 工作准备
$ ssh qwert@host && cd ~ && git clone ssh://git@host/home/git/repository/repo.git
$ git config user.name "qwert"
$ git config user.email "qwert@example.com"

// poiuy 工作准备
$ ssh poiuy@host && cd ~ && git clone ssh://git@host/home/git/repository/repo.git
$ git config user.name "poiuy"
$ git config user.email "poiuy@example.com"

功性分支下开发阶段

开发者 qwert

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 创建功能分支
$ git checkout -b dev-user-menu
$ vim README.md
1. this is the second commit.
2. poiuy
3. qwert
4. qwert feature start.
5. poiuy feature start.
6. qwert another feature start.
7. poiuy tract release v1.0
8. qwert hotfix start.
9. qwert develop user menu.
$ git add README.md
$ git commit -m "Develop user menu."
$ git push -u origin master

开发者 poiuy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 创建功能分支
$ git checkout -b dev-user-auth
$ vim README.md
1. this is the second commit.
2. poiuy
3. qwert
4. qwert feature start.
5. poiuy feature start.
6. qwert another feature start.
7. poiuy tract release v1.0
8. qwert hotfix start.
9. poiuy develop user auth.
$ git add README.md
$ git commit -m "Develop user auth"
$ git push -u origin dev-user-auth

一段时间以后,qwertpoiuy 都声称把各个负责的功能开发完成,并提交到远程仓库了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 发布开发者 qwert 的功能到测试环境测试,测试通过后合并到 master 分支
$ git checkout master
$ git pull # 拉取其它开发者的更新
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ssh://host/home/git/repository/repo
 * [new branch]      dev-user-auth -> origin/dev-user-auth
Already up-to-date.
$ git checkout dev-user-menu
$ git rebase master # 用 rebase 合并主干的修改,如果有冲突在此时解决
$ git checkout master

// 将 qwert 新开发的功能合并到 master 分支
$ git merge dev-user-menu
Updating 40e19f1..4f92cff
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)
$ git push origin master # 将合并后的提交到远程 master 分支

// 开发者 poiuy 的也通过测试,需要将代码合并到 master 分支
$ git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ git pull
From ssh://host/home/git/repository/repo
   40e19f1..4f92cff  master     -> origin/master
Updating 40e19f1..4f92cff
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)

// 切换到功能分支,用 rebase 合并主干的修改,之前由于 qwert 和 poiuy 编辑同一个区域,无疑会产生冲突
$ git checkout dev-user-auth
$ git rebase master # 用 rebase 合并主干的修改,如果有冲突在此时解决
First, rewinding head to replay your work on top of it...
Applying: Develop user auth
Using index info to reconstruct a base tree...
M  README.md
Falling back to patching base and 3-way merge...
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Failed to merge in the changes.
Patch failed at 0001 Develop user auth
The copy of the patch that failed is found in:
   /home/poiuy/repo/.git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
$ git rebase --continue # 动用 rebase --continue 命令逐个修复冲突文件,直到 No rebase in progress? 为止
$ git add README.md
$ git commit -m "Conflict resolution"
$ git push origin dev-user-auth
Counting objects: 10, done.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 679 bytes | 0 bytes/s, done.
Total 6 (delta 1), reused 0 (delta 0)
To ssh://git@host/home/git/repository/repo.git
   ccbab61..f0aea38  dev-user-auth -> dev-user-auth

// 开发者 poiuy 切换到 master 分支,将冲突后的修复合并回 master
$ git checkou master
$ git merge dev-user-auth
Updating 4f92cff..f0aea38
Fast-forward
 README.md | 5 +++++
 1 file changed, 5 insertions(+)
$ git push origin master

// 此时 qwert 再切换到 master 进行拉取,至此两个开发者的各个功能都合并到 master 分支了,可以发布 release 了
$ git checkou master
$ git pull
remote: Counting objects: 10, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From ssh://localhost/home/git/repository/repo
   4f92cff..f0aea38  master     -> origin/master
   ccbab61..f0aea38  dev-user-auth -> origin/dev-user-auth
Updating 4f92cff..f0aea38
Fast-forward
 README.md | 5 +++++
 1 file changed, 5 insertions(+)

显而易见,这个分支只需要维护 master 分支就可以了,也就是说,在合并到 master 分支之前,需要确保对功能进行充分的测试,因为 master 分支是可以随时 release 的,而发布和相关的补丁都基于 master 分支,所以在分支达到生产要求后,就要立即合并到 master 分支,删除功能分支,再基于 master 分支创建新的分支进行新一轮的开发。当然,这个流程有个弊端就是,当我们 Code Review 之后,准备在 master 释放 release 时,又有人向 master 分支提交了新的完成功能,或者有些公司指定日期时间发布,等到指定的那个时间,master 分支已经有了新的合并而打乱最初的发布意图。因为这个时候,我们的本意是把上一个功能完全发布后再进行下一个版本的发布。为了应付这种情况,就需要在 master 分支以外,新建一个 production 分支来跟踪线上版本,也就是说,在 git 这儿没有什么是一个分支搞不定的,如果有,哪就再创建一个分支。

功能分支工作流的使用场景:

  • 开发团队相对固定,而且有一定规模
  • 常常有多个功能,多个问题并行开发
  • 对代码审查有较高要求
  • 更注重团队效率

通过 Pull Requests 讨论

一旦某个开发者完成一个功能,不是立即合并到 master,而是 push 到远程仓库的功能分支上并发起一个 Pull Request 请求去合并修改到 master。在修改成为主干代码前,这让其它的开发者有机会先去 Review 变更。

Pull Request 可以和 feature 分支工作流、Gitflow 工作流或者 Forking 工作流一起使用。但 Pull Request 需要两个不同的分支或是两个不同的仓库,因此它们不能和中心化的工作流一起使用。在不同的工作流中使用 Pull Request 有些不同,但大致的流程如下:

  • 开发者在他们的本地仓库中为某个功能创建一个专门的分支
  • 开发者将分支推送到公共的 GitHub 仓库
  • 开发者用 GitHub 发起一个 Pull Request
  • 其余的团队成员审查代码,讨论并且做出修改
  • 项目维护者将这个功能并入官方的仓库,然后关闭这个 Pull Request

为了演示这个功能,我注册了两个 GitHub 来说明这个情况,假设 qwertpoiuy 是两个 GitHub 用户,并且已经注册

  • 创建项目

qwert 创建一个项目

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1024-fb-pr-pro-create.jpg

随后 qwert 邀请 poiuy 加入

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1025-fb-pr-collaborator.jpg

poiuy 接受邀请

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1026-fb-pr-invitation.jpg

  • 拉取项目
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// qwert 拉取项目
$ git clone https://github.com/imajinyun/test.git

// qwert 设置用户名邮箱
$ cd test
$ git config user.name "qwert"
$ git config user.email "qwert@example.com"

// qwert 初始化项目
$ echo "1. initialize project." >> README.md
$ git add README.md
$ git commit -m "Add README.md"
$ git push -u origin master

// poiuy 拉取项目
$ git clone https://github.com/imajinyun/test.git

// poiuy 设置用户名邮箱
$ cd test
$ git config user.name "poiuy"
$ git config user.email "poiuy@example.com"
  • 功能开发
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// qwert 开发某个功能
$ git pull
$ git checkou -b dev-user-menu
$ vim README.md
1. initialize project.
2. qwert develop user menu.
$ git commit -m "Develop user menu."
$ git push -u origin dev-user-menu

// poiuy 开发某个功能
$ git pull
$ git checkou -b dev-user-auth
$ vim README.md
1. initialize project.
2. poiuy develop user auth.
$ git commit -m "Develop user auth."
$ git push -u origin dev-user-auth
  • 各自发起 Pull Request

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1027-fb-pr-new.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1028-fb-pr-finish.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1029-fb-pr-open.jpg

  • qwert 对发起的 Pull Request 进行合并

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1030-fb-pr-merged-by-qwert.jpg

  • poiuy 对发起的 Pull Request 进行合并

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1031-fb-pr-resolved-by-poiuy.jpg

这时我们发现冲突了,需要先解决冲突再合并:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1032-fb-pr-mark-resolved.jpg

poiuy 解决完冲突进行合并:

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1033-fb-pr-resolved-conflicts.jpg

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1034-fb-pr-resolved-conflicts-merged.jpg

终于两个功能都合并到 master 分支了。

https://inotes.oss-cn-beijing.aliyuncs.com/git/201812/1035-fb-pr-master.jpg

至此 Pull Request 算是完结了,至于其它两个工作流程上都差不多,就不再赘述了。

用一篇文章说清楚 Git Workflow 是不太现实的,我吭哧吭哧的写了一堆,仅代表我对 Git Workflow 的理解和使用方法。

参考