一杯茶的時間,上手 Git 團隊協作開發

tuture發表於2020-05-13

我們研發開源了一款基於 Git 進行技術實戰教程寫作的工具,我們圖雀社群的所有教程都是用這款工具寫作而成,歡迎 Star

如果你想快速瞭解如何使用,歡迎閱讀我們的 教程文件哦

本文由圖雀社群成員 mRc 寫作而成,歡迎加入圖雀社群,一起創作精彩的免費技術教程,予力程式設計行業發展。

在大多數工作中,我們都將使用 Git 作為團隊協作開發的工具。

本文總結了圖雀團隊協作開發的流程與規範,僅供參考。最優的解決方案還是需要結合團隊的實際情況,具體問題具體分析。

為了讓大家能夠非常清晰直觀地瞭解協作開發的流程,大家在看的時候可以開啟 Learn Git Branching 的沙箱執行環境來實踐(可以直接輸入提供的程式碼)。在左邊的終端中輸入命令,就會在右邊看到相應的動畫。其中左邊的實線圓圈代表本地倉庫,右邊的虛線圓圈代表遠端倉庫,* 號指向的是當前分支,o/master 就是遠端分支( o 就相當於 origin )。

由於 Learn Git Branching 為了演示和學習的方便對部分命令做了簡化,我將指出在實際操作中應當輸入的命令。

基本流程

接下來將重點講述以下兩個流程:

  • 貢獻程式碼

  • 更新本地倉庫

貢獻程式碼

接下來的流程描述了在接到開發任務後,如何為中心倉庫貢獻程式碼。

將倉庫 clone 到本地

$ git clone

實際命令應當提供 URI 引數,例如:

$ git clone https://github.com/dhucst/cooperation.git

開啟新分支

$ git checkout -b B1

為了便於演示,我們將新分支命名為 B1。在實際開發中,新分支的命名應當遵循以下原則:

  • 使用 kebab-case,例如 new-branch ,而不是 new_branchnewBranch

  • 儘量能概括這個分支所要完成的任務

  • 如果是為了解決某個 Issue,在最後加上 Issue 的編號,例如 fix-75

編寫程式碼並提交

$ git commit

實際命令應當要先執行 git add 來將修改的檔案新增到暫存區,例如:

$ git add .
$ git commit

Commit Message (Log) 的書寫是有比較嚴格的規範的,會在後文的 提交資訊書寫規範 中詳細闡述。

推送分支
$ git push

實際命令在第一次 push 任何分支時,應當指定 remote 和分支名稱:

$ git push origin B1

有時候我們的分支會在一夜之間“過時”。什麼是過時的分支,我們該怎樣處理?不要方,後面會講到。

提交 Pull Request

這一步驟無需在 Learn Git Branching 中操作。

將分支提交到遠端倉庫後,開啟倉庫的 GitHub 頁面,應該會看到下面這樣黃色的提示框:

image-20200429091859922

然後點選 Compare & pull request 按鈕,即可進入到提交 Pull Request 頁面。

填寫 Pull Request 標題所遵循的原則與 Commit message 大致相似。在填寫 Pull Request 的詳細內容時,如果是為了解決某個或多個 Issue 時,可以使用 Close(s), Fix(es)Resolve(s) 關鍵詞來關閉某個 Issue,例如 Fix #75

點選 Create pull request 按鈕後,即可完成本次 PR。如果經討論後發現需要修改,則在本地倉庫修改後直接 git push 繼續提交即可。如果程式碼通過了評審,則會由專案管理者將此分支併入 master 中,本次貢獻程式碼流程結束。

更新本地倉庫

接下來的流程介紹了當團隊其他成員貢獻程式碼後,如何將遠端倉庫的更新同步到本地。

如果你在使用 Learn Git Branching 邊看邊練,請輸入以下命令:

$ reset
$ git clone

其他成員貢獻程式碼

$ git fakeTeamwork 2

實際沒有這條 Git 命令 ?,是 Learn Git Branching 提供用於練習協作的。

這時候你會發現遠端的倉庫有了本地沒有的提交 C2C3

拉取遠端程式碼

我們先來看第一種比較簡單的情況:

這時候一眼就可以看出,只需把遠端的 C2C3 直接拉取過來接在本地的 C1 後面就可以了:

$ git pull

接著我們來看另一種比較棘手的情況:

需要輸入的命令如下:

$ reset
$ git clone
$ git checkout -b B2
$ git commit
$ git fakeTeamwork 2

對著圖看,我們在 B2 分支上在開發某個新功能,這時候遠端倉庫已經更新到了 C4,很顯然我們本地的 master 分支和 B2 分支都不是最新的了。這種情況很常見:幾個小夥伴從同一個起點(在這裡就是 C1 )各自開發新功能時,其他人先於我們提交。

大多數情況下,請遵循這一條原則:只更新 master 分支。

這一原則對於並行開發並不適用,我們會在本知識庫後續文件中講解。

$ git checkout master
$ git pull

這時候,我們就會認為 B2 分支已經過時(outdated),因為它沒有最新的 C3C4 。但過時的分支並不意味著沒有價值了,我們可以像前面所講解的那樣 push 到遠端倉庫:

$ git checkout B2
$ git push

然後一樣可以發起 Pull Request。GitHub 會提示你這條分支已經過時,你可以點選 Update Branch 按鈕來更新這一條分支(通常由專案管理者來執行這一操作)。

小結

團隊協作開發的模型只涉及兩個核心流程:貢獻程式碼和更新本地倉庫。

貢獻程式碼的流程:

$ git clone <REPO_URI>
$ git checkout -b new-branch
$ git add .
$ git commit
$ git push origin new-branch

更新程式碼庫的流程:

$ git checkout master
$ git pull

並行開發

一個專案的開發往往由多個開發任務組成,每個人都會負責承擔一個或多個開發任務。最簡單、最理想的情況當然是:同學 A 開始貢獻程式碼,成功合併後所有人更新原生程式碼庫;接著同學 B 開始貢獻程式碼,合併後所有人更新原生程式碼庫;然後是同學 C、D、E……

不會有任何衝突,只需用到前面 基本流程 所介紹到的命令,多麼輕鬆愉快!

唯一的問題就是:這樣的開發顯然進度很慢,而且大家的時間安排也不夠自由。這種序列開發的方式過於同步化,對於一個追求效率的團隊來說是不能接受的。我們需要高度並行完全非同步的協作開發模式。

接下來我們將描述三個典型的並行開發場景,其中的主角是大唐同學和煨鴿同學。

互不依賴且沒有修改同一檔案

例如有個著陸頁開發的任務,大唐負責做“關於我們”頁面,叫 about-us.html,煨鴿負責做“聯絡我們”頁面,叫 contact.html,這兩個檔案相互獨立的。

這裡我們假定大唐同學率先完成了任務並且已經合併到 origin/master 。這時候根據前一章 更新本地倉庫 一節的說法,煨鴿正在工作的分支已經“過時”。這時候他只需要繼續完成他的 contact.html 頁面,然後提交就可以了。

這是最簡單的,也是最常見的情況(合理的任務劃分應當如此):相互獨立的分支只需依次 push,不管是否過時。

存在依賴關係且沒有修改同一檔案

現在我們又假設大唐在開發著陸頁的首頁 index.html,煨鴿負責寫著陸頁的樣式 index.css,很明顯大唐的開發任務依賴煨鴿。經過一天的開發,大唐寫完了主體部分 C2,煨鴿也寫好了樣式 C3 並且已經提交到遠端倉庫,現在他需要把煨鴿的樣式表加進來,才能完成自己的開發任務。

Learn Git Branching 中輸入以下程式碼:

$ git clone
$ git checkout -b html
$ git commit
$ git fakeTeamwork

然後大唐使用 fetch 命令將遠端的 C3 抓取下來(其實更嚴格的說法是將本地的 o/master 分支與遠端的 master 同步):

$ git fetch

可以看到,html 檔案的提交和 css 檔案的提交在不同的分支上。html 是我們工作的分支(也是當前所在的分支),因此要把 C3 所在的 o/master 合併過來:

$ git merge o/master

這個形狀看上去有點嚇人!實際上,你只要真正理解分支的本質就會覺得非常好理解。

分支不能簡單地理解為一串 commit(雖然說在大多數情況下這種理解非常直觀),而應該理解為指向某個 commit 的指標,而該 commit 的所有父節點都是該分支上的節點(commit)。因此在執行合併後,我們可以說 C2C3 都已經在 html 分支上了。

合併之後,我們再修改點東西,提交為 C5 ,然後推送到遠端倉庫:

$ git commit
$ git push

再次提醒真正的 push 命令在第一次推送某一分支時要加上遠端倉庫名稱和分支名稱,例如 git push origin html

接下來就是提交 Pull Request、等待合併就可以了。

修改同一檔案

首先宣告這種情況非常少見,合理的任務劃分會盡量避免這種情況出現。但是我們還是會講解一下這種比較棘手的情況。由於 Learn Git Branching 沒有提供衝突(conflict)的演示,所以我們需要自己在本地開倉庫進行演示。

為什麼在本地開倉庫練習就可以了,而不需要搭一個遠端倉庫嗎?因為本小節的操作流程和命令跟上一節相比,除了增加了一個處理衝突的步驟,其餘完全相同,因此我們關注的重點是怎麼處理衝突。

$ mkdir conflict-demo && cd conflict-demo
$ git init
$ touch index.js
$ git add .
$ git commit -m "Add index.js"

然後我們開啟一個新分支 add-func :

$ git checkout -b add-func

在 index.js 中增添一個 add 函式:

function add(x, y) {
  return x + y;
}

儲存並提交:

$ git add .
$ git commit -m "Implement add function"

然後我們切回主分支,並開啟一個叫 origin-master 的分支(聽這名字也知道,它模擬了遠端的主分支):

$ git checkout master
$ git checkout -b origin-master

接著再在 index.js 中新增一個叫 multiply 的函式:

function multiply(x, y) {
  return x * y;
}

好了,現在本地的 add-func 工作分支和“遠端”的 origin-master 分支修改了同一檔案 index.js,衝突一觸即發!讓我們來點燃這根導火索!

其實你可以不停地把分支切來切去(輪流輸入 git checkout add-funcgit checkout origin-master ),你會看到 index.js 的內容會隨之變來變去,版本控制系統的魅力可見一斑。

$ git checkout add-func
$ git merge origin-master

我們會發現 Git 會輸出你從未見過的資訊:

Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.

劃重點:index.js 在合併時發生衝突,請處理衝突然後提交。

我們檢視 index.js 的內容,發現了很神奇的東西(在命令列中用 cat 檢視):

<<<<<<< HEAD
function add(x, y) {
  return x + y;
=======
function multiply(x, y) {
  return x * y;
>>>>>>> origin-master
}

如果我們用 VSCode 開啟,會看到更炫酷的結果:

這就一目瞭然了!綠色部分是我們當前分支 add-func 的內容,藍色部分是 origin-master 的內容。由於我們兩者都要,所以點選 Accept Both Changes。然後略經修正,將 index.js 改為如下:

function add(x, y) {
  return x + y;
}

function multiply(x, y) {
  return x * y;
}

提交我們用於處理衝突的 commit:

$ git add .
$ git commit -m "Merge conflict of index.js"

衝突處理完成,我們提交此分支,任務完成。

小結

並行開發是 Git 團隊協作中比較高階卻又非常重要的部分。平時大多數情況下,我們遇到的都是第一種情況。如果“不幸”遇到了後面兩種情況,不熟悉時可以回來看一看這篇文件。

PS:對於後面兩種情況,有一點需要補充:如果想要撤銷 merge,使用下面這條命令:

$ git merge --abort

提交資訊書寫規範

提交資訊,又稱為 commit messages 或者 commit logs,是每一步提交所必需的資訊。我們可以看一下 React 倉庫的提交記錄:

由此我們可以對專案每一步做了什麼有了比較好的瞭解。

格式

每次提交,Commit message 都包括三個部分:Header,Body 和 Footer。

<header>
// 空一行
<body>
// 空一行
<footer>

其中,Header 是必需的,Body 和 Footer 可以省略。

Header

Header部分只有一行,是對 commit 的簡短概述,是一個包括動賓結構修改物件(可選)祈使句_(不要加句號!)_。

我們來看幾個例子。

Remove 'warning' module from the JS scheduler

這裡的動賓結構是 Remove ‘warning’ module,修改物件是 JS scheduler。

Add @flow directive to findDOMNode shim

這裡的動賓結構是 Add @flow directive,修改物件是 findDOMNode shim。

Update www warning shim

這裡動賓結構是 Update www warning shim,由於修改物件已經很明確(在動賓結構中),所以無需再寫。

Body

Body 部分是對本次 commit 的詳細描述,可以分成多行。下面是一個範例。

More detailed explanatory text, if necessary.  Wrap it to 
about 72 characters or so. 

Further paragraphs come after blank lines.

- Bullet points are okay, too
- Use a hanging indent

有兩個注意點。

  • 使用第一人稱現在時,比如使用 change 而不是 changedchanges

  • 應該說明程式碼變動的動機,以及與以前行為的對比。

Footer

Footer 部分只用於兩種情況。

(1)不相容變動

如果當前程式碼與上一個版本不相容,則 Footer 部分以 BREAKING CHANGE 開頭,後面是對變動的描述、以及變動理由和遷移方法。

(2)關閉 Issue

如果當前 commit 針對某個issue,那麼可以在 Footer 部分關閉這個 issue。

Closes #234

也可以一次關閉多個 issue 。

Closes #123, #245, #992

我們團隊建議在 Pull Request 中關閉 Issue,如前面基本流程所描述的那樣。

Revert

還有一種特殊情況,如果當前 commit 用於撤銷以前的 commit,則必須以 revert: 開頭,後面跟著被撤銷 Commit 的 Header。

revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body 部分的格式是固定的,必須寫成 This reverts commit <hash>.,其中的 hash 是被撤銷 commit 的 SHA 識別符號。

如何修改

一開始寫 Commit Message 的時候難免會出現寫得不好的情況,一般情況下會有人建議你如何寫得更好,或者你自己想到了更合適的寫法。這時候該如何修改呢?

修改最近一次提交

如果你要修改的就是最近一次提交,那就非常簡單了。Git 有專門的命令用於輕鬆修改剛才的提交:

$ git commit --amend

然後就會進入 vi 介面重新編輯你的提交資訊。當然也可以直接用 -m 選項指定提交資訊:

$ git commit --amend -m "Updated commit message"

想要在 Learn Git Branching 看看怎麼回事?輸入下面的命令體驗一下吧:

$ git commit
$ git commit --amend

修改倒數第 n 次的提交

下面要介紹的 rebase 命令威力可以說是非常巨大,但是要掌握卻實屬不易。沒事,我們先來看看如何用 rebase 修改倒數第 3 次提交:

$ git rebase -i HEAD~3

-i 的意思是 --interactive ,輸入後 Git 就會開啟一個 vi 編輯器,並出現下面的內容:

pick 0f78800 倒數第4次提交
pick 459014c 倒數第3次提交
pick 38009c7 倒數第2次提交
pick dff7f7d 最新的提交

# Rebase 500d110..dff7f7d onto 500d110 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

其實如果你認真看一下下面的註釋,基本上知道怎麼做了:把倒數第三次提交前面的 pick 命令改為 reword 。儲存後,Git 就會把你帶到倒數第 3 次提交的 vi 編輯頁面,這時候重新寫提交資訊就可以了。

你也可以在 Learn Git Branching 中體驗一下 rebase 命令:

$ reset
$ git commit
$ git commit
$ git rebase -i HEAD~3

Learn Git Branching 提供的是圖形化介面,和 Git 的 vi 介面略有區別。

強制推送修改

有時候可能你已經把分支 push 到遠端倉庫、甚至已經提交了 Pull Request 了。如果直接 push ,Git 會因為遠端和本地的分支衝突而拒絕推送。這時候只需要加上 -f 選項,強制用本地的分支覆蓋遠端的分支即可:

$ git push -f

參考

阮一峰《Commit message 和 Change log 編寫指南》

程式碼評審

程式碼評審,又稱程式碼審查,是軟體開發流程中必不可少的一環。

程式碼審查是計算機原始碼的系統性檢驗(有時被稱為同行評審)。其目的在於找到開發初期所忽略的錯誤,從而提高軟體的整體質量。
——Wikipedia

為什麼要程式碼評審

程式碼評審並不意味著被評審者的能力不足。有下面這些原因表明程式碼評審的重要性。

降低風險

寫出存在 bug 的程式碼再正常不過了。每個人貢獻的程式碼先要經過持續整合(CI,Continuous Integration)的一系列構建測試,然後是人工程式碼審查,因此程式碼審查可以說是最後一道防線。

顯著提高程式碼質量

程式碼評審不僅僅是單純地查詢 bug 或是修正格式問題,還包括使程式碼更高效。

在一個團隊裡,每個人都有自己的背景和特長,因此總有人可能提出更聰明的解決方案,更合適的設計模式,或者能降低複雜性或提高效能的方法。

有助於熟悉專案

當一個團隊在做一個專案時,想要每個開發人員致力於應用的每個部分,這是極不可能的。有時候,會出現這種情況:在某一段時間,一個開發人員正為專案的大部分模組辛苦地工作,而另一個人則完全在做別的東西。

知識共享

通過合作,每個人都可以相互學習並取得進步。提交程式碼者很有可能從該工作中得到反饋,並意識到可能存在的問題和需要改進的部分;而審查者也可以通過閱讀他人程式碼學到新的東西,並找出適用於他們自己的工作方案。

如何進行程式碼評審

發起程式碼評審

程式碼評審發生在 Pull Request 階段,程式碼提交者可以請求其他成員的 Review,如下圖所示。

然後被請求進行評審的成員開啟這條 Pull Request 頁面時會出現一個提示框:

我們點選 Add your review 按鈕,即進入到 Review 頁面(或者也可以點選 Files changed 這個 Tab)。Review 頁面展示了本次 Pull Request 所有發生改動的檔案,評審的過程也就是審查這些發生改動的程式碼。

在 GitHub 上評審

直接在 GitHub 的 Pull Request 頁面評審是最基本的方法。對於改動比較小的分支,這種方法完全足夠。

有時候我們發現了他人程式碼的問題。千萬不要保留你的意見!要把自己的想法有條理地寫下來。我們可以選擇特定一行來發表評論,只需把滑鼠移到行首,就會顯示一個加號,如下圖。

點選加號,就可以對這一行進行評論了:

拉取到本地評審

有時候某些分支的改動非常大,大到需要你在本地親自執行一下,看看是否真的達到了預期的目標。

$ git fetch origin lots-of-changes
$ git checkout logs-of-changes

然後你的本地倉庫就完成切換到待評審分支的狀態了!你可以試著執行,做各種嘗試,還可以在自己熟悉的編輯器裡面更加舒適地閱讀程式碼,美滋滋。

提交評審結果

無論是直接在 GitHub 還是在本地審查,最後都要提交評審結果。評審結果包括你在程式碼行中的所有評論、Review summary 和最終意見。

Review summary 主要是一些總結性的話語。如果程式碼提交者確實做得非常優秀,當然是要誇獎一下喔;如果有些地方做得不足,則要給出改進的方向和一些鼓勵。

最終意見有以下三種:

  • Comment:只是做一些客觀評價,對此分支是否可以合併不給出明確意見

  • Approve:同意此分支合併進主分支

  • Request changes:不同意此分支合併,需要進一步修改

接著程式碼提交者根據其他人的評審進行修改後提交,然後再繼續評審,如此迭代,直到分支可以合併。

最佳實踐

對於程式碼提交者

任務最小化

每個開發任務都應當只做一件事情,因此所需評審的程式碼應可能地少。事實表明,超過 200 行的程式碼評審的有效性顯著降低,超過 400 行時程式碼評審幾乎沒有意義。

提供足夠的上下文

在編寫程式碼時應有意識地新增足夠的註釋或文件,因為你的程式碼會被很多人閱讀。良好的註釋能夠讓團隊其他成員評審你的程式碼時更加輕鬆,也更容易發現問題所在。另外,在填寫 Pull Request 說明資訊時,也應該將所解決的問題、發生的相應改變說明清楚。

對於評審者

評審最重要的事情

不要糾結於程式碼風格或是格式問題,這些事情會有專門的工具代勞。你應當關注的是下面這些問題:

  • 程式碼是否具備良好的可讀性?

  • 能否實現得更簡潔、更地道?

  • 程式碼是否遵循了良好的設計原則?

  • 程式碼的空間效率和時間效率怎麼樣?

保持積極開放的心態

不必過於挑剔,樂於讚揚他人的勞動,學會欣賞他人的程式碼。

想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

圖雀社群

相關文章