Files
CategoryResourceRepost/极客时间专栏/研发效率破局之道/个人效能/26 | Facebook怎样实现代码提交的原子性?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

42 KiB
Raw Blame History

你好我是葛俊。今天我们继续来聊聊如何通过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 = git@github.com: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步

  1. 一个原子性的功能完成后,使用第25篇文章中提到的改变提交顺序的方法把它放到距离origin/master最近的地方。
  2. 把这个提交发到代码审查系统Phabricator上进行质量检查包括代码审查和机器检查。在等待质量检查结果的同时继续其他提交的开发。
  3. 如果没有通过质量检查则需要对提交进行修改修改之后返回第2步。
  4. 如果通过质量检查, 就把这个提交推送到主代码仓的共享分支上然后继续其他分支的开发回到第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 <gejun_1978@yahoo.com>
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的细节。

阶段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 <gejun_1978@yahoo.com>
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两个提交了。

接下来我需要进行需求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是为了区别之前的B1B1仍然存在代码仓中不过是不再使用了而已。

阶段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进行原子性拆分而已在实际工作中很可能功能实现和文档就应该放在一个提交当中。

提交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的操作是重新应用产生出了三个新提交。

现在我可以把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仍然存在只是不在提交链里了而已。

最后运行命令把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三个提交了。

阶段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 三个提交了。

接下来,我使用第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 一个提交。

然后我对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) jasonge@Juns-MacBook-Pro-2.local:~/jksj-repo/git-atomic-demo
> git show
commit 29c82490256459539c4a1f79f04823044f382d2b (HEAD)
Author: Jason Ge <gejun_1978@yahoo.com>
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 一个提交。

最后运行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 三个提交。

阶段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

解决冲突的具体步骤是:

  1. 手动修改冲突文件;
  2. 使用git add或者git rm把修改添加到暂存区
  3. 运行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 三个提交。

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

这时我从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 一个提交。

我终于可以运行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

至此我们完成了在一个分支上同时开发两个需求A和B、把提交拆分为原子性提交并尽早把完成的提交推送到远端代码仓共享分支的全过程

这个过程看起来比较复杂但实际上就是根据上面列举的“单分支工作流”的4个步骤执行而已。

接下来,我们再看看使用多个分支,每个分支支持一个需求的开发方式。

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

在这种开发工作流下,每个需求都拥有独享的分支。同样的,跟单分支实现提交原子性的方式一样,这些分支都是本地分支,并不是主代码仓上的功能分支。

需要注意的是,在下面的分析中,我只描述每个分支上只有一个提交的简单形式,而至于每个分支上使用多个提交的形式,操作流程与单分支提交链中的描述一样,就不再重复表述了。

多分支工作流具体步骤

分支工作流的具体步骤大致包括以下4步

  1. 切换到某一个分支对某需求进行开发,产生提交。
  2. 提交完成后将其发送到代码审查系统Phabricator上进行质量检查。在等待质量检查结果的同时切换到其他分支继续其他需求的开发。
  3. 如果第2步的提交没有通过质量检查则切换回这个提交所在分支对提交进行修改修改之后返回第2步。
  4. 如果第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一个提交。

阶段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两个提交。

阶段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一个提交。

阶段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自动化意义重大。

至此我们完成了在两个分支上同时开发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个状态。

思考题

  1. 在对提交链的非当前提交比如HEAD^进行修改时除了使用git rebase -i在它暂停的时候进行修改你还其他办法吗你觉得这种方法的利弊是什么
  2. Git和文字编排系统LaTex有一个有趣的共同点你知道是什么吗

感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再见!