极客时间已完结课程限时免费阅读

26 | Facebook怎样实现代码提交的原子性?

26 | Facebook怎样实现代码提交的原子性?-极客时间

26 | Facebook怎样实现代码提交的原子性?

讲述:葛俊

时长24:27大小22.39M

你好,我是葛俊。今天,我们继续来聊聊如何通过 Git 提高代码提交的原子性吧。
在上一篇文章中,我给你详细介绍了 Git 助力提高代码提交原子性的五条基础操作,今天我们再来看看 Facebook 的开发人员具体是如何使用这些操作来实现提交的原子性的。
为了帮助你更直观地理解、学习,在这篇文章里,我会与你详细描述工作场景,并列出具体命令。同时,我还把这些命令的输出也都放到了文章里,供你参考。所以,这篇文章会比较长、比较细。不过不要担心,这些内容都是日常工作中的自然流程,阅读起来也会比较顺畅。
在 Facebook,开发人员最常使用两种 Git 工作流:
使用一个分支,完成所有需求的开发;
使用多个分支,每个分支支持一个需求的开发。
两种工作流都利用 Git 的超强功能来提高代码原子性。这里的“需求”包括功能开发和缺陷修复,用大写字母 A、B、C 等表示;每个需求都可能包含有多个提交,每个提交用需求名 + 序号表示。比如,A 可能包含 A1、A2 两个提交,B 只包含 B1 这一个提交,而 C 包含 C1、C2、C3 三个提交。
需要强调的是,这两种工作流中的一个分支和多个分支,都是在开发者本地机器上的分支,不是远程代码仓中的功能分支。我在前面第 7 篇文章中提到过,Facebook 的主代码仓是不使用功能分支的。
另外,这两种 Git 工作流对代码提交原子性的助力作用,跟主代码仓是否使用单分支开发没有关系。也就是说,即使你所在团队的主仓没有使用 Facebook 那样的单分支开发模式,仍然可以使用这两种工作流来提高代码提交的原子性。
接下来,我们就先看看第一种工作流,也就是使用一个分支完成所有需求的开发。

工作流一:使用一个分支完成所有需求的开发

这种工作流程的最大特点是,使用一个分支上的提交链,大量使用 git rebase -i 来修改提交链上的提交。这里的提交链,指的是当前分支上,还没有推送到远端主仓共享分支的所有提交。
首先,我们需要设置一个本地分支来开发需求,通过这个分支和远端主仓的共享分支进行交互。本地分支通常直接使用 master 分支,而远端主仓的共享分支一般是 origin/master,也叫作上游分支(upstream)。
一般来说,在 git clone 的时候,master 是默认已经产生,并且是已经跟踪 origin/master 了的,你不需要做任何设置,可以查看.git/config 文件做确认:
> cat .git/config
...
[remote "origin"]
url = [email protected]:jungejason/git-atomic-demo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
可以看到,branch "master"里有一个 remote = origin 选项,表明 master 分支在跟踪 origin 这个上游仓库;另外,config 文件里还有一个 remote "origin"选项,列举了 origin 这个上游仓库的地址。
当然,除了直接查看 config 文件外,Git 还提供了命令行工具。你可以使用 git branch -vv 查看某个分支是否在跟踪某个远程分支,然后再使用 git remote show去查看远程代码仓的细节。
## 查看远程分支细节
> git branch -vv
master 5055c14 [origin/master: behind 1] Add documentation for getRandom endpoint
## 查看分支跟踪的远程代码仓细节
> git remote show origin
* remote origin
Fetch URL: git@github.com:jungejason/git-atomic-demo.git
Push URL: git@github.com:jungejason/git-atomic-demo.git
HEAD branch: master
Remote branch:
master tracked
Local branches configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (fast-forwardable)
11:07:36 (master2) jasonge@Juns-MacBook-Pro-2.local:~/jksj-repo/git-atomic-demo
因为 config 文件简单直观,所以我常常直接到 config 文件里面查看和修改来完成这些操作。关于远程跟踪上游代码仓分支的更多细节,比如产生新分支、设置上游分支等,你可以参考Git: Upstream Tracking Understanding这篇文章。
设置好分支之后,我们来看看这个工作流中的具体步骤

单分支工作流具体步骤

单分支工作流的步骤,大致包括以下 4 步:
一个原子性的功能完成后,使用第 25 篇文章中提到的改变提交顺序的方法,把它放到距离 origin/master 最近的地方。
把这个提交发到代码审查系统 Phabricator 上进行质量检查,包括代码审查和机器检查。在等待质量检查结果的同时,继续其他提交的开发。
如果没有通过质量检查,则需要对提交进行修改,修改之后返回第 2 步。
如果通过质量检查, 就把这个提交推送到主代码仓的共享分支上,然后继续其他分支的开发,回到第 1 步。
请注意第二步的目的是,确保入库代码的质量,你可以根据实际情况进行检查。比如,你可以通过提交 PR 触发机器检查的工作流,也可以运行单元测试自行检查。如果没有任何质量检查的话,至少也要进行简单手工验证,让进入到远程代码仓的代码有起码的质量保障。
接下来,我设计了一个案例,尽量模拟我在 Facebook 的真实开发场景,与你讲述这个工作流的操作步骤。大致场景是这样的:我本来在开发需求 A,这时来了更紧急的需求 B。于是,我开始开发 B,把 B 分成两个原子性提交 B1 和 B2,并在 B1 完成之后最先推送到远程代码仓共享分支。
这个案例中,提交的改动很简单,但里面涉及了很多开发技巧,可供你借鉴。

阶段 1:开始开发需求 A

某天,我接到开发需求 A 的任务,要求在项目中添加一个 README 文件,对项目进行描述。
我先添加一个简单的 README.md 文件,然后用 git commit -am ‘readme’ 快速生成一个提交 A1,确保代码不会丢失。
## 文件内容
> cat README.md
## This project is for demoing git
## 产生提交
> git commit -am 'readme'
[master 0825c0b] readme
1 file changed, 1 insertion(+)
create mode 100644 README.md
## 查看提交历史
> git log --oneline --graph
* 0825c0b (HEAD -> master) readme
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
...
## 查看提交细节
> git show
commit 0825c0b6cd98af11b171b52367209ad6e29e38d1 (HEAD -> master)
Author: Jason Ge <[email protected]>
Date: Tue Oct 15 12:45:08 2019
readme
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..789cfa9
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+## This project is for demoing git
这时,A1 是 master 上没有推送到 origin/master 的唯一提交,也就是说,是提交链上的唯一提交。
请注意,A1 的 Commit Message 很简单,就是“readme”这 6 个字符。在把 A1 发出去做代码质量检查之前,我需要添加 Commit Message 的细节。
图 1 提交链状态第 1 步

阶段 2:开始开发需求 B

这时,来了另外一个紧急需求 B,要求是添加一个 endpoint getRandom。开发时,我不切换分支,直接在 master 上继续开发。
首先,我写一个 getRandom 的实现,并进行简单验证。
## 用VIM修改
> vim index.js
## 查看工作区中的改动
> git diff
diff --git a/index.js b/index.js
index 986fcd8..06695f6 100644
--- a/index.js
+++ b/index.js
@@ -6,6 +6,10 @@ app.get('/timestamp', function (req, res) {
res.send('' + Date.now())
})
+app.get('/getRandom', function (req, res) {
+ res.send('' + Math.random())
+})
+
app.get('/', function (req, res) {
res.send('hello world')
})
## 用命令行工具httpie验证结果
> http localhost:3000/getRandom
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 19
Content-Type: text/html; charset=utf-8
Date: Tue, 15 Oct 2019 03:49:15 GMT
ETag: W/"13-U1KCE8QRuz+dioGnmVwMkEWypYI"
X-Powered-By: Express
0.25407324324864167
为确保代码不丢失,我用 git commit -am ‘random’ 命令生成了一个提交 B1:
## 产生提交
> git commit -am 'random'
[master 7752df4] random
1 file changed, 4 insertions(+)
## 查看提交历史
> git log --oneline --graph
* 7752df4 (HEAD -> master) random
* 0825c0b readme
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
...
## 查看提交细节
> git show
commit f59a4084e3a2c620bdec49960371f8cc93b86825 (HEAD -> master)
Author: Jason Ge <[email protected]>
Date: Tue Oct 15 11:55:06 2019
random
diff --git a/index.js b/index.js
index 986fcd8..06695f6 100644
--- a/index.js
+++ b/index.js
@@ -6,6 +6,10 @@ app.get('/timestamp', function (req, res) {
res.send('' + Date.now())
})
+app.get('/getRandom', function (req, res) {
+ res.send('' + Math.random())
+})
+
app.get('/', function (req, res) {
res.send('hello world')
})
B1 的 Commit Message 也很简陋,因为当前的关键任务是先把功能运行起来。
现在,我的提交链上有 A1 和 B1 两个提交了。
图 2 提交链状态第 2 步
接下来,我需要进行需求 B 的进一步开发:在 README 文件中给这个新的 endpoint 添加说明。
> git diff
diff --git a/README.md b/README.md
index 789cfa9..7b2b6af 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
## This project is for demoing git
+
+You can visit endpoint getRandom to get a random real number.
我认为这个改动是 B1 的一部分,所以我用 git commit --amend 把它添加到 B1 中。
## 添加改动到B1
> git add README.md
> git commit --amend
[master 27c4d40] random
Date: Tue Oct 15 11:55:06 2019 +0800
2 files changed, 6 insertions(+)
## 查看提交历史
> git log --oneline --graph
* 27c4d40 (HEAD -> master) random
* 0825c0b readme
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
现在,我的提交链上还是 A1 和 B1’两个提交。这里的 B1’是为了区别之前的 B1,B1 仍然存在代码仓中,不过是不再使用了而已。
图 3 提交链状态第 3 步

阶段 3:拆分需求 B 的代码,把 B1’提交检查系统

这时,我觉得 B1’的功能实现部分,也就是 index.js 的改动部分,可以推送到 origin/master 了。
不过,文档部分也就是 README.md 文件的改动,还不够好,而且功能实现和文档应该分成两个原子性提交。于是,我将 B1’拆分为 B1’’ 和 B2 两部分。
## 将B1'拆分
> git reset HEAD^
Unstaged changes after reset:
M README.md ## 这个将是B2的内容
M index.js ## 这个将是B1''的内容
> git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: README.md
modified: index.js
no changes added to commit (use "git add" and/or "git commit -a")
> git add index.js
> git commit ## 这里我认真填写B1''的Commit Message
> git add README.md
> git commit ## 这里我认真填写B2的Commit Message
## 查看提交历史
* 68d813f (HEAD -> master) [DO NOT PUSH] Add documentation for getRandom endpoint
* 7d43442 Add getRandom endpoint
* 0825c0b readme
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
现在,提交链上有 A1、B1’’、B2 三个提交。
请注意,在这里我把功能实现和文档分为两个原子性提交,只是为了帮助说明我需要把 B1’进行原子性拆分而已,在实际工作中,很可能功能实现和文档就应该放在一个提交当中。
图 4 提交链状态第 4 步
提交 B1’拆开之后,为了把 B1’’ 推送到 origin/master 上去,我需要要把 B1’’ 挪到 A1 的前面。首先,运行 git rebase -i origin/master。
> git rebase -i origin/master
## 下面是弹出的编辑器
pick 0825c0b readme ## 这个是A1
pick 7d43442 Add getRandom endpoint ## 这个是B1''
pick 68d813f [DO NOT PUSH] Add documentation for getRandom endpoint
# Rebase 7b6ea30..68d813f onto 7b6ea30 (3 commands)
...
然后,我把针对 B1’’ 的那一行挪到第一行,保存退出。
pick 7d43442 Add getRandom endpoint ## 这个是B1''
pick 0825c0b readme ## 这个是A1
pick 68d813f [DO NOT PUSH] Add documentation for getRandom endpoint
# Rebase 7b6ea30..68d813f onto 7b6ea30 (3 commands)
...
git rebase -i 命令会显示运行成功,使用 git log 命令可以看到我成功改变了提交的顺序。
> git rebase -i origin/master
Successfully rebased and updated refs/heads/master.
> git log --oneline --graph
* 86126f7 (HEAD -> master) [DO NOT PUSH] Add documentation for getRandom endpoint
* 7113c16 readme
* 4d37768 Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
现在,提交链上有 B1’’’、A1’、B2’三个提交了。请注意,B2’也是一个新的提交。虽然我只是交换了 B1’’ 和 A 的顺序,但 git rebase 的操作是重新应用,产生出了三个新提交。
图 5 提交链状态第 5 步
现在,我可以把 B1’’’ 发送给质量检查系统了。
首先,产生一个临时分支 temp 指向 B2’,确保能回到原来的代码;然后,用 git reset --hard 命令把 master 和 HEAD 指向 B1’’’。
> git branch temp
> git reset --hard 4d37768
HEAD is now at 4d37768 Add getRandom endpoint
## 检查提交链
> git log --oneline --graph
* 4d37768 (HEAD -> master) Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
...
这时,提交链中只有 B1’’’。当然,A1’和 B2’仍然存在,只是不在提交链里了而已。
图 6 提交链状态第 6 步
最后,运行命令把 B1’’’ 提交到 Phabricator 上,结束后使用 git reset --hard temp 命令重新把 HEAD 指向 B2’。
## 运行arc命令把B'''提交到Phabricator上
> arc diff
## 重新把HEAD指向B2'
> git reset --hard temp
HEAD is now at 86126f7 [DO NOT PUSH] Add documentation for getRandom endpoint
## 检查提交链
> git log --oneline --graph
* 86126f7 (HEAD -> master, temp, single-branch-step-5) [DO NOT PUSH] Add documentation for getRandom endpoint
* 7113c16 readme
* 4d37768 Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
这时,提交链又恢复成为 B1’’’、A1’、B2’三个提交了。
图 7 提交链状态第 7 步

阶段 4:继续开发 B2,同时得到 B1 的反馈,修改 B1

把 B1’’’ 发送到质量检查中心之后,我回到 B2’ 继续工作,也就是在 README 文件中继续添加关于 getRandom 的文档。我正在开发的过程中,得到 B1’’’ 的反馈,要求我对其进行修改。于是,我首先保存当前对 B2’的修改,用 git commit --amend 把它添加到 B2’中。
## 查看工作区中的修改
> git diff
diff --git a/README.md b/README.md
index 8a60943..1f06f52 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
## This project is for demoing git
You can visit endpoint getRandom to get a random real number.
+The end endpoint is `/getRandom`.
## 把工作区中的修改添加到B2'中
> git add README.md
> git commit --amend
[master 7b4269c] [DO NOT PUSH] Add documentation for getRandom endpoint
Date: Tue Oct 15 17:17:18 2019 +0800
1 file changed, 3 insertions(+)
* 7b4269c (HEAD -> master) [DO NOT PUSH] Add documentation for getRandom endpoint
* 7113c16 readme
* 4d37768 Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
...
这时,提交链成为 B1’’’、A1’、B2’’ 三个提交了。
图 8 提交链状态第 8 步
接下来,我使用第 25 篇文章中介绍的基础操作对 B1’’’ 进行修改。
首先,在 git rebase -i origin/master 的文本输入框中,将 pick B1’’’ 那一行修改为 edit B1’’’,然后保存退出,git rebase 暂停在 B1’’’ 处:
> git rebase -i origin/master
## 以下是弹出编辑器中的文本内容
edit 4d37768 Add getRandom endpoint ## <-- 这一行开头原本是pick
pick 7113c16 readme
pick 7b4269c [DO NOT PUSH] Add documentation for getRandom endpoint
## 以下是保存退出后 git rebase -i origin/master 的输出
Stopped at 4d37768... Add getRandom endpoint
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
## 查看提交历史
> git log --oneline --graph
* 4d37768 (HEAD) Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
这时,提交链上只有 B1’’’ 一个提交。
图 9 提交链状态第 9 步
然后,我对 index.js 进行修改,并添加到 B1’’’ 中,成为 B1’’’’。完成之后,再次把 B1’’’’ 发送到代码质量检查系统。
## 根据同事反馈,修改index.js
> vim index.js
> git add index.js
## 查看修改
> git diff --cached
diff --git a/index.js b/index.js
index 06695f6..cc92a42 100644
--- a/index.js
+++ b/index.js
@@ -7,7 +7,7 @@ app.get('/timestamp', function (req, res) {
})
app.get('/getRandom', function (req, res) {
- res.send('' + Math.random())
+ res.send('The random number is:' + Math.random())
})
app.get('/', function (req, res) {
## 把改动添加到B1'''中。
> git commit --amend
[detached HEAD 29c8249] Add getRandom endpoint
Date: Tue Oct 15 17:16:12 2019 +0800
1 file changed, 4 insertions(+)
19:17:28 (master|REBASE-i) [email protected]:~/jksj-repo/git-atomic-demo
> git show
commit 29c82490256459539c4a1f79f04823044f382d2b (HEAD)
Author: Jason Ge <[email protected]>
Date: Tue Oct 15 17:16:12 2019
Add getRandom endpoint
Summary:
As title.
Test:
Verified it on localhost:3000/getRandom
diff --git a/index.js b/index.js
index 986fcd8..cc92a42 100644
--- a/index.js
+++ b/index.js
@@ -6,6 +6,10 @@ app.get('/timestamp', function (req, res) {
res.send('' + Date.now())
})
+app.get('/getRandom', function (req, res) {
+ res.send('The random number is:' + Math.random())
+})
+
app.get('/', function (req, res) {
res.send('hello world')
})
## 查看提交链
> git log --oneline --graph
* 29c8249 (HEAD) Add getRandom endpoint
* 7b6ea30 (origin/master, git-add-p) Add a new endpoint to return timestamp
## 将B1''''发送到代码审查系统
> arc diff
这时,提交链只有 B1’’’’ 一个提交。
图 10 提交链状态第 10 步
最后,运行 git rebase --continue 完成整个 git rebase -i 操作。
> git rebase --continue
Successfully rebased and updated refs/heads/master.
## 查看提交历史
> git log --oneline --graph
* bc0900d (HEAD -> master) [DO NOT PUSH] Add documentation for getRandom endpoint
* 1562cc7 readme
* 29c8249 Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
...
这时,提交链包含 B1’’’’、A1’’、B2’’’ 三个提交。
图 11 提交链状态第 11 步

阶段 5:继续开发 A1,并发出代码审查

这时,我认为 A1’’ 比 B2’’’ 更为紧急重要,于是决定先完成 A1’’ 的工作并发送审查,同样也是使用 git rebase -i。
> git rebase -i HEAD^^ ## 两个^^表示从当前HEAD前面两个提交的地方rebase
## git rebase 弹出编辑窗口
edit 1562cc7 readme <-- 这一行开头原来是pick。这个是A1''
pick bc0900d [DO NOT PUSH] Add documentation for getRandom endpoint
## 保存退出后git rebase -i HEAD^^ 的结果
Stopped at 1562cc7... readme
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
## 对A1''修改
> vim README.md
> git diff
diff --git a/README.md b/README.md
index 789cfa9..09bcc7d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-## This project is for demoing git
+# This project is for demoing atomic commit in git
> git add README.md
> git commit --amend
## 下面是git commit弹出编辑器,在里面完善A1''的Commit Message
Add README.md file
Summary: we need a README file for the project.
Test: none.
# Please enter the Commit Message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Tue Oct 15 12:45:08 2019 +0800
#
# interactive rebase in progress; onto 29c8249
# Last command done (1 command done):
# edit 1562cc7 readme
# Next command to do (1 remaining command):
# pick bc0900d [DO NOT PUSH] Add documentation for getRandom endpoint
# You are currently splitting a commit while rebasing branch 'master' on '29c8249'.
#
# Changes to be committed:
# new file: README.md
#
## 保存退出后git commit 输出结果
[detached HEAD 2c66fe9] Add README.md file
Date: Tue Oct 15 12:45:08 2019 +0800
1 file changed, 1 insertion(+)
create mode 100644 README.md
## 继续执行git rebase -i
> git rebase --continue
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: could not apply bc0900d... [DO NOT PUSH] Add documentation for getRandom endpoint
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply bc0900d... [DO NOT PUSH] Add documentation for getRandom endpoint
这个过程可能会出现冲突,比如在 A1’’’ 之上应用 B2’’’ 时可能会出现冲突。冲突出现时,你可以使用 git log 和 git status 命令查看细节。
## 查看当前提交链
> git log --oneline --graph
* 2c66fe9 (HEAD) Add README.md file
* 29c8249 Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
...
## 查看冲突细节
> git status
interactive rebase in progress; onto 29c8249
Last commands done (2 commands done):
edit 1562cc7 readme
pick bc0900d [DO NOT PUSH] Add documentation for getRandom endpoint
No commands remaining.
You are currently rebasing branch 'master' on '29c8249'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: README.md
no changes added to commit (use "git add" and/or "git commit -a")
## 用git diff 和git diff --cached查看更多细节
> git diff
diff --cc README.md
index 09bcc7d,1f06f52..0000000
--- a/README.md
+++ b/README.md
@@@ -1,1 -1,4 +1,8 @@@
++<<<<<<< HEAD
+# This project is for demoing atomic commit in git
++=======
+ ## This project is for demoing git
+
+ You can visit endpoint getRandom to get a random real number.
+ The end endpoint is `/getRandom`.
++>>>>>>> bc0900d... [DO NOT PUSH] Add documentation for getRandom endpoint
> git diff --cached
* Unmerged path README.md
解决冲突的具体步骤是:
手动修改冲突文件;
使用 git add 或者 git rm 把修改添加到暂存区;
运行 git rebase --continue。于是,git rebase 会把暂存区的内容生成提交,并继续 git-rebase 后续步骤。
> vim README.md
## 这个是初始内容
<<<<<<< HEAD
# This project is for demoing atomic commit in git
=======
## This project is for demoing git
You can visit endpoint getRandom to get a random real number.
The end endpoint is `/getRandom`.
>>>>>>> bc0900d... [DO NOT PUSH] Add documentation for getRandom endpoint
## 这个是修改后内容,并保存退出
# This project is for demoing atomic commit in git
You can visit endpoint getRandom to get a random real number.
The end endpoint is `/getRandom`.
## 添加README.md到暂存区,并使用git status查看状态
> git add README.md
19:51:16 (master|REBASE-i) jasonge@Juns-MacBook-Pro-2.local:~/jksj-repo/git-atomic-demo
## 使用git status查看状态
> git status
interactive rebase in progress; onto 29c8249
Last commands done (2 commands done):
edit 1562cc7 readme
pick bc0900d [DO NOT PUSH] Add documentation for getRandom endpoint
No commands remaining.
You are currently rebasing branch 'master' on '29c8249'.
(all conflicts fixed: run "git rebase --continue")
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: README.md
## 冲突成功解决,继续git rebase -i后续步骤
> git rebase --continue
## git rebase 提示编辑B2''''Commit Message
[DO NOT PUSH] Add documentation for getRandom endpoint
Summary:
AT.
Test:
None.
## 保存退出之后git rebase --continue的输出
[detached HEAD ae38d9e] [DO NOT PUSH] Add documentation for getRandom endpoint
1 file changed, 3 insertions(+)
Successfully rebased and updated refs/heads/master.
## 检查提交链
* ae38d9e (HEAD -> master) [DO NOT PUSH] Add documentation for getRandom endpoint
* 2c66fe9 Add README.md file
* 29c8249 Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
这时,提交链上有 B1’’’’、A1’’’、B2’’’’ 三个提交。
图 12 提交链状态第 12 步

阶段 6:B1 检查通过,推送到远程代码仓共享分支

这时,我从 Phabricator 得到通知,B1’’’’ 检查通过了,可以将其推送到 oringin/master 去了!
首先,使用 git fetch 和 git rebase origin/master 命令,确保本地有远端主代码仓的最新代码。
> git fetch
> git rebase origin/master
Current branch master is up to date.
然后,使用 git rebase -i,在 B1’’’’ 处暂停:
> git rebase -i origin/master
## 修改第一行开头:pick -> edit
edit 29c8249 Add getRandom endpoint
pick 2c66fe9 Add README.md file
pick ae38d9e [DO NOT PUSH] Add documentation for getRandom endpoint
## 保存退出结果
Stopped at 29c8249... Add getRandom endpoint
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
## 查看提交链
* 29c8249 (HEAD) Add getRandom endpoint
* 7b6ea30 (origin/master) Add a new endpoint to return timestamp
...
这时,origin/master 和 HEAD 之间只有 B1’’’’ 一个提交。
图 13 提交链状态第 13 步
我终于可以运行 git push origin HEAD:master,去推送 B1’’’’ 了。
注意,当前 HEAD 不在任何分支上,master 分支仍然指向 B2’’’’,所以 push 命令需要明确指向远端代码仓 origin 和远端分支 maser,以及本地要推送的分支 HEAD。推送完成之后,再运行 git rebase --continue 完成 rebase 操作,把 master 分支重新指向 B2’’’’。
## 直接推送。因为当前HEAD不在任何分支上,推送失败。
> git push
fatal: You are not currently on a branch.
To push the history leading to the current (detached HEAD)
state now, use
git push origin HEAD:<name-of-remote-branch>
## 再次推送,指定远端代码仓origin和远端分支maser,以及本地要推送的分支HEAD。推送成功
> git push origin HEAD:master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 392 bytes | 392.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:jungejason/git-atomic-demo.git
7b6ea30..29c8249 HEAD -> master
> git rebase --continue
Successfully rebased and updated refs/heads/master.
## 查看提交链
> git log --oneline --graph
* ae38d9e (HEAD -> master) [DO NOT PUSH] Add documentation for getRandom endpoint
* 2c66fe9 Add README.md file
* 29c8249 (origin/master) Add getRandom endpoint
* 7b6ea30 Add a new endpoint to return timestamp
这时,origin/master 已经指向了 B1’’’’,提交链现在只剩下了 A1’’’ 和 B2’’’’。
图 14 提交链状态第 14 步
至此,我们完成了在一个分支上同时开发两个需求 A 和 B、把提交拆分为原子性提交,并尽早把完成的提交推送到远端代码仓共享分支的全过程!
这个过程看起来比较复杂,但实际上就是根据上面列举的“单分支工作流”的 4 个步骤执行而已。
接下来,我们再看看使用多个分支,每个分支支持一个需求的开发方式。

用本地多分支实现多个需求的提交的原子性

在这种开发工作流下,每个需求都拥有独享的分支。同样的,跟单分支实现提交原子性的方式一样,这些分支都是本地分支,并不是主代码仓上的功能分支。
需要注意的是,在下面的分析中,我只描述每个分支上只有一个提交的简单形式,而至于每个分支上使用多个提交的形式,操作流程与单分支提交链中的描述一样,就不再重复表述了。

多分支工作流具体步骤

分支工作流的具体步骤,大致包括以下 4 步:
切换到某一个分支对某需求进行开发,产生提交。
提交完成后,将其发送到代码审查系统 Phabricator 上进行质量检查。在等待质量检查结果的同时,切换到其他分支,继续其他需求的开发。
如果第 2 步的提交没有通过质量检查,则切换回这个提交所在分支,对提交进行修改,修改之后返回第 2 步。
如果第 2 步的提交通过了质量检查,则切换回这个提交所在分支,把这个提交推送到远端代码仓中,然回到第 1 步进行其他需求的开发。
接下来,我们看一个开发两个需求 C 和 D 的场景吧。
在这个场景中,我首先开发需求 C,并把它的提交 C1 发送到质量检查中心;然后开始开发需求 D,等到 C1 通过质量检查之后,我立即将其推送到远程共享代码仓中去。

阶段 1:开发需求 C

需求 C 是一个简单的重构,把 index.js 中所有的 var 都改成 const。
首先,使用 git checkout -b feature-c origin/master 产生本地分支 feature-c,并跟踪 origin/master。
> git checkout -b feature-c origin/master
Branch 'feature-c' set up to track remote branch 'master' from 'origin'.
Switched to a new branch 'feature-c'
然后,进行 C 的开发,产生提交 C1,并把提交发送到 Phabricator 进行检查。
## 修改代码,产生提交
> vim index.js
> git diff
diff --git a/index.js b/index.js
index cc92a42..e5908f0 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,6 @@
-var port = 3000
-var express = require('express')
-var app = express()
+const port = 3000
+const express = require('express')
+const app = express()
app.get('/timestamp', function (req, res) {
res.send('' + Date.now())
20:54:10 (feature-c) jasonge@Juns-MacBook-Pro-2.local:~/jksj-repo/git-atomic-demo
> git add .
20:54:16 (feature-c) jasonge@Juns-MacBook-Pro-2.local:~/jksj-repo/git-atomic-demo
> git commit
## 填写详细Commit Message
Refactor to use const instead of var
Summary: const provides more info about a variable. Use it when possible.
Test: ran `node index.js` and verifeid it by visiting localhost:3000.
Endpoints still work.
## 以下是Commit Message保存后退出,git commit的输出结果
[feature-c 2122faa] Refactor to use const instead of var
1 file changed, 3 insertions(+), 3 deletions(-)
## 使用Phabricator的客户端,arc,把当前提交发送给Phabricator进行检查
> arc diff
## 查看提交链
* 2122faa (HEAD -> feature-c, multi-branch-step-1) Refactor to use const instead of var
* 5055c14 (origin/master) Add documentation for getRandom endpoint
...
这时,origin/master 之上只有 feature-c 一个分支,上面有 C1 一个提交。
图 15 多分支提交状态第 1 步

阶段 2:开发需求 D

C1 发出去进行质量检查后,我开始开发需求 D。需求 D 是在 README.md 中,添加所有 endpoint 的文档。
首先,也是使用 git checkout -b feature-d origin/master 产生一个分支 feature-d 并跟踪 origin/master。
> git checkout -b feature-d origin/master
Branch 'feature-d' set up to track remote branch 'master' from 'origin'.
Switched to a new branch 'feature-d'
Your branch is up to date with 'origin/master'.
然后,开始开发 D,产生提交 D1,并把提交发送到 Phabricator 进行检查。
## 进行修改
> vim README.md
## 添加,产生修改,过程中有输入Commit Message
> git add README.md
> git commit
## 查看修改
> git show
commit 97047a33071420dce3b95b89f6d516e5c5b59ec9 (HEAD -> feature-d, multi-branch-step-2)
Author: Jason Ge <gejun_1978@yahoo.com>
Date: Tue Oct 15 21:12:54 2019
Add spec for all endpoints
Summary: We are missing the spec for the endpoints. Adding them.
Test: none
diff --git a/README.md b/README.md
index 983cb1e..cbefdc3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,8 @@
# This project is for demoing atomic commit in git
-You can visit endpoint getRandom to get a random real number.
-The end endpoint is `/getRandom`.
+## endpoints
+
+* /getRandom: get a random real number.
+* /timestamp: get the current timestamp.
+* /: get a "hello world" message.
## 将提交发送到Phabricator进行审查
> arc diff
## 查看提交历史
> git log --oneline --graph feature-c feature-d
* 97047a3 (HEAD -> feature-d Add spec for all endpoints
| * 2122faa (feature-c) Refactor to use const instead of var
|/
* 5055c14 (origin/master) Add documentation for getRandom endpoint
这时,origin/master 之上有 feature-c 和 feature-d 两个分支,分别有 C1 和 D1 两个提交。
图 16 多分支提交状态第 2 步

阶段 3:推送提交 C1 到远端代码仓共享分支

这时,我收到 Phabricator 的通知,C1 通过了检查,可以推送了!首先,我使用 git checkout 把分支切换回分支 feature-c:
> git checkout feature-c
Switched to branch 'feature-c'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
然后,运行 git fetch; git rebase origin/master,确保我的分支上有最新的远程共享分支代码:
> git fetch
> git rebase origin/master
Current branch feature-c is up to date.
接下来,运行 git push 推送 C1:
> git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 460 bytes | 460.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:jungejason/git-atomic-demo.git
5055c14..2122faa feature-c -> master
## 查看提交状态
* 97047a3 (feature-d) Add spec for all endpoints
| * 2122faa (HEAD -> feature-c, origin/master, multi-branch-step-1) Refactor to use const instead of var
|/
* 5055c14 Add documentation for getRandom endpoint
...
这时,origin/master 指向 C1。分支 feature-d 从 origin/master 的父提交上分叉,上面只有 D1 一个提交。
图 17 多分支提交状态第 3 步

阶段 4:继续开发 D1

完成 C1 的推送后,我继续开发 D1。首先,用 git checkout 命令切换回分支 feature-d;然后,运行 git fetch 和 git rebase,确保当前代码 D1 是包含了远程代码仓最新的代码,以减少将来合并代码产生冲突的可能性。
> git checkout feature-d
Switched to branch 'feature-d'
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
21:38:22 (feature-d) jasonge@Juns-MacBook-Pro-2.local:~/jksj-repo/git-atomic-demo
> git fetch
> git rebase origin/master
First, rewinding head to replay your work on top of it...
Applying: Add spec for all endpoints
## 查看提交状态
> git log --oneline --graph feature-c feature-d
* a8f92f5 (HEAD -> feature-d) Add spec for all endpoints
* 2122faa (origin/master,) Refactor to use const instead of var
...
这时,当前分支为 feature-d,上面有唯一一个提交 D1’,而且 D1’已经变基到了 origin/master 上。
需要注意的是,因为使用的是 git rebase,没有使用 git merge 产生和并提交,所以提交历史是线性的。我在第 7 篇文章中提到过,线性的提交历史对 Facebook 的 CI 自动化意义重大。
图 18 多分支提交状态第 4 步
至此,我们完成了在两个分支上同时开发 C 和 D 两个需求,并尽早把完成了的提交推送到远端代码仓中的全过程。
虽然在这个例子中,我简化了这两个需求开发的情况,每个需求只有一个提交并且一次就通过了质量检查,但结合在一个分支上完成所有开发需求的流程,相信你也可以推导出每个需求有多个提交,以及质量检查没有通过时的处理方法了。如果这中间还有什么问题的话,那就直接留言给我吧。
接下来,我与你对比下这两种工作流。

两种工作流的对比

如果我们要对比这两工作流的话,那就是各有利弊。
单分支开发方式的好处是,不需要切换分支,可以顺手解决一些缺陷修复,但缺点是 rebase 操作多,产生冲突的可能性大。
而多分支方式的好处是,一个分支只对应一个需求,相对比较简单、清晰,rebase 操作较少,产生冲突的可能性小,但缺点是不如单分支开发方式灵活。
无论是采用哪一种工作流,都有几个需要注意的地方:
不要同时开发太多的需求,否则分支管理花销太大;
有了可以推送的提交就尽快推送到远端代码仓,从而减少在本地的管理成本,以及推送时产生冲突的可能性;
经常使用 git fetch 和 git rebase,确保自己的代码在本地持续与远程共享分支的代码在做集成,降低推送时冲突的可能性
最后,我想说的是,如果你对 Git 不是特别熟悉,我推荐你先尝试第二种工作流。这种情况 rebase 操作较少,相对容易上手一些。

小结

今天,我与你详细讲述了,在 Facebook 开发人员借助 Git 的强大功能,实现代码提交的原子性的两种工作流。
第一种工作流是,在一个单独的分支上进行多个需求的开发。总结来讲,具体的工作方法是:把每一个需求的提交都拆分为比较小的原子提交,并使用 git rebase -i 的功能,把可以进行质量检查的提交,放到提交链的最底部,也就是最接近 origin/master 的地方,然后发送到代码检查系统进行检查,之后继续在提交链的其他提交处工作。如果提交没有通过检查,就对它进行修改再提交检查;如果检查通过,就马上把它推送到远端代码仓的共享分支去。在等待代码检察时,继续在提交链的其他提交处工作。
第二种工作流是,使用多个分支来开发多个需求,每个分支对应一个需求。与单分支开发流程类似,我们尽快把当前可以进行代码检查的提交,放到离 origin/master 最近的地方;然后在代码审查时,继续开发其他提交。与单分支开发流程不同的是,切换工作任务时,需要切换分支。
这两种工作流,无论哪一种都能大大促进代码提交的原子性,从而同时提高个人及团队的研发效能。
我把今天的案例放到了 GitHub 上的git-atomic-demo代码仓里,并标注出了各个提交状态产生的分支。比如,single-branch-step-14 就是单分支流程中的第 14 个状态,multi-branch-step-4 就是多分支流程中的第 4 个状态。

思考题

在对提交链的非当前提交,比如 HEAD^,进行修改时,除了使用 git rebase -i,在它暂停的时候进行修改,你还其他办法吗?你觉得这种方法的利弊是什么?
Git 和文字编排系统 LaTex 有一个有趣的共同点,你知道是什么吗?
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再见!
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 3

提建议

上一篇
25 | 玩转Git:五种提高代码提交原子性的基本操作
下一篇
27 | 命令行:不只是酷,更重要的是能提高个人效能
 写留言

精选留言(6)

  • Johnson
    2019-10-23
    第一种太烧脑了,对开发人员的git技能要求太高了,大部分的开发人员都不能正确驾驭,感觉工作中更多的是以第二种为主,然后在某个branch内部结合第一种的情况比较容易驾驭。很头疼的问题是很多老同事没有深入学习git的主动性和激情,稍微复杂点儿的操作就让别人帮忙操作。

    作者回复: >第二种为主,然后在某个branch内部结合第一种的情况比较容易驾驭 的确是这样。第1种操作的确是要求比较高。而且如果有比较多的提交的话,rebase的overhead会比较大。所以单独的分支上不推荐,产生太多的提交。 > 很头疼的问题是很多老同事没有深入学习git的主动性和激情 如果他们能够意识到git的强大能够帮助自己提高研发效率的话,可能就会更愿意投入一些。能不能给他们搞点集体培训什么的?对团队效能提升帮助应该不错

    共 3 条评论
    4
  • 于小咸
    2019-11-03
    葛老师,请问提交代码的原子性,除了提高git技巧外,在工程上有什么需要注意的点吗?比如软件架构,设计模式。这方面应该随着语言和项目的不同会有较大的差异,有没有什么通用的原则?

    作者回复: 工程上,主要是解耦和增量开发。解耦好理解,把功能进行拆分。而增量开发这个一般大家提的不多。主要是在实现功能的时候,考虑怎么样实现最小子功能,即时这个子功能不能暴露给用户。我以后抽空详细写一下,通过代码的实际例子来解释应该更清楚。

    2
  • Weining Cao
    2019-10-23
    从来不用rebase, 一直用merge,看来要好好学习下rebase了

    作者回复: http://onlywei.github.io/explain-git-with-d3/#rebase 交互式的界面学rebase。推荐看看 :)

    2
  • 我来也
    2019-10-23
    学习了,这两种方式我都会,也经常用。 我比较喜欢线性的提交历史,不喜欢merge。 但有个比较纠结的是。 比如采用第二种方式: 单独开发某个功能,也是采用小步走的方式,这样一个功能分支开发完后,可能有很多小的commit。 如果这些commit都线性的推到远程,感觉也不好。 目前只能先自己用rebase合并一些分支,然后再推。 如果是使用git merge —no-ff的方式,这些小commit就可以保留,在历史中可以看到完整的开发过程。 但是这样分支看上去就临时分叉了。 不知道老师推荐用哪种方法。 课后思考题一 我能想到的笨办法: 1。先备份当前分支,比如git branch temp 2。git reset —hard HEAD^ 3。修改并git commit —amend —no-edit 4。git cherry-pick 后面的commit 😄 或者git checkout temp ;git rebase xxx 刚才修改后的commit (不知道可不可行)
    展开

    作者回复: 1----- > 但有个比较纠结的是... 我一般是使用为rebase或者cherry-pick,把这些分支上的都放到合并到一个分支上,不使用merge --no-ff命令。 至于提交的粒度,就要考虑原子性了。尽量是单个提交只完成单个需求。当然,有些时候如果bug fix很小,也可以选择把多个bug fix合并成一个提交。 2----- > 1。先备份当前分支,比如git branch temp > 2。git reset —hard HEAD^ > 3。修改并git commit —amend —no-edit > 4。git cherry-pick 后面的commit 😄 > 或者git checkout temp ;git rebase xxx 刚才修改后的commit (不知道可不可行) 这两种办法都可以,都很好! 还有第三个办法,我一般是把提交交换顺序,把要修改的提交放到HEAD处修改。 另外,我还第一次见到--no-edit。我以前都是用 commit --amend -a -C HEAD来完成类似功能 :)

    共 2 条评论
    2
  • 浇了汁鸡
    2021-11-29
    还有一种合并方式叫做'半线性历史记录',(https://stackoverflow.com/questions/59714347/semi-linear-merge),先把特性分支基于master做rebase,再merge --no-ff,既可以看到每次合并引入点,又能近似拥有线性历史的定位问题的便捷性
  • 巫山老妖
    2021-04-18
    第一种虽然能够不切换分支完成原子性提交,但感觉复杂性也变高了,要求所有人都能掌握还是比较困难,目前我们团队还是以第二种为主,不过有些同学还是习惯用merge来合并分支