理解Git工作流

zhongqi發表於2012-08-09

如果你不理解Git的設計動機,那你就會處處碰壁。知道足夠多的命令和引數後,你就會強行讓Git按你想的來工作,而不是按Git自己的方式來。這就像把螺絲刀當錘子用;也能把活幹完,但肯定乾的差極了,花費很長時間,還會弄壞螺絲刀。

想想常見的Git工作流程是怎麼失效的吧。

從Master建立一個分支,寫程式碼,然後把這個分支合併回Master。

多數時候這樣做的效果會如你所願,因為從你建立分支到合併回去之間,Master一般都會有些變動。然後,有一天當你想把一個功能(feature)分支合併進Master的時候,而Master並沒有像以往那樣有變動,問題來了:這時Git不會進行合併commit,而是將Master指向功能分支上的最新commit。(看圖

不幸的是,你的功能分支有用來備份程式碼的commit(作者稱之為checkpoint commit),這些經常進行的commit對應的程式碼可能處於不穩定狀態!而這些commit現在沒法和Master上那些穩定的commit區分開來了。當你想回滾的時候,很容易發生災難性後果。

於是你就記住了:“當合並功能分支的時候,加上 -no-ff 選項強制進行一次全新的commit。”嗯,這麼做好像解決問題了,那麼繼續。

然後一天你線上上環境中發現了一個嚴重bug,這時你需要追溯下這個bug是什麼時候引入的。你執行了bisect命令,但卻總是追溯到一些不穩定的commit。因此你不得不放棄,改用人肉檢查。

最後你將bug範圍縮小到一個檔案。你執行blame命令檢視這個檔案在過去48小時裡的變動。然後blame告訴你這個檔案已經好幾周沒有被修改過了——你知道根本不可能沒有變動。哦,原來是因為blame計算變動是從第一次commit算起,而不是merge的時候。你在幾周前的一次commit中改動了這個檔案,但這個變動今天才被merge回來。

用no-ff來救急,bisect又臨時失效,blame的運作機制又那麼模糊,所有這些現象都說明一件事兒,那就是你正在把螺絲刀當錘子用。

反思版本控制

版本控制的存在是因為兩個原因。

首先,版本控制是用來輔助寫程式碼的。因為你要和同事同步程式碼,並經常備份自己的程式碼。當然了,把檔案壓縮後發郵件也行,不過工程大了大概就不好辦了。

其次,就是輔助配置管理工作。其中就包括並行開發的管理,比如一邊給線上版本修復bug,一邊開發下一個版本。配置管理也可以幫助弄清楚變動發生的具體時間,在追溯bug中是一個很好的工具。 一般說來,這兩個原因是衝突的。

在開發一個功能的時候,你應該經常做備份性的commit。然而,這些commit經常會讓軟體沒法編譯。 理想情況是,你的版本更新歷史中的每一次變化都是明確且穩定的,不會有備份性commit帶來的噪聲,也不會有超過一萬行程式碼變動的超大commit。一個清晰的版本歷史讓回滾和選擇性merge都變得相當容易,而且也方便以後的檢查和分析。然而,要維護這樣一個乾淨的歷史版本庫,也許意味著總是要等到程式碼完善之後才能提交變動。

那麼,經常性的commit和乾淨的歷史,你選擇哪一個?

如果你是在剛起步的創業公司中,乾淨的歷史沒有太大幫助。你可以放心地把所有東西都往Master中提交,感覺不錯的時候隨時釋出。

如果團隊規模變大或是使用者規模擴大了,你就需要些工具和技巧來做約束,包括自動化測試,程式碼檢查,以及乾淨的版本歷史。

功能分支貌似是一個不錯的折中選擇,能夠基本的並行開發問題。當你寫程式碼時候,可以不用怎麼在意整合的問題,但它總有煩到你的時候。

當你的專案規模足夠大的時候,簡單的branch/commit/merge工作流程就出問題了。縫縫補補已經不行了。這時你需要一個乾淨的版本歷史庫。

Git之所以是革命性的,就是因為它能同時給你這兩方面的好處。你可以在原型開發過程中經常備份變動,而搞定後只需要交付一個乾淨的版本歷史。

工作流程

考慮兩種分支:公共的和私有的。

公共分支是專案的權威性歷史庫。在公共分支中,每一個commit都應該確保簡潔、原子性,並且有完善的提交資訊。此分支應該儘可能線性,且不能更改。公共分支包括Master和發行版的分支。

私有分支是供你自己使用的,就像解決問題時的草稿紙。

安全起見,把私有分支只儲存在本地。如果你確實需要push到伺服器的話(比如要同步你在家和辦公室的電腦),最好告訴同事這是私有的,不要基於這個分支展開工作。

絕不要直接用merge命令把私有分支合併到公共分支中。要先用reset、rebase、squash merges、commit amending等工具把你的分支清理一下。

把你自己看做一個作者,每一次的commit視為書中的一章。作者不會出版最初的草稿,就像Michael Crichton說的,“偉大的書都不是寫出來——而是改出來的”。

如果你沒接觸過Git,那麼修改歷史對你來說好像是種禁忌。你習慣於認為提交過的所有東西都應該像刻在石頭上一樣不能抹去。但如果按這種邏輯,我們在文字處理軟體器中也不應該使用“撤銷”功能了。 實用主義者們直到變化變為噪音的時候才關注變化。對於配置管理來說,我們關注巨集觀的變化。日常commit(checkpoint commits)只是備份於雲端的用於“撤銷”的緩衝。

如果你保持公共歷史版本庫的簡潔,那麼所謂的fast-forward merge就不僅安全而且可取了,它能保證版本變更歷史的線性和易於追溯。

關於 -no-ff 僅剩的爭論就只剩“文件證明”了。人們可能會先merge再commit,以此代表最新的線上部署版本。不過,這是反模式的。用tag吧。

規則和例子

根據改變的多少、持續工作時間的長短,以及分支分叉了多遠,我使用三種基本的方法。

1)短期工作

絕大多數時間,我做清理時只用squash merge命令。

假設我建立了一個功能分支,並且在接下來一個小時裡進行了一系列的checkpoint commit。

git checkout -b private_feature_branch
touch file1.txt
git add file1.txt
git commit -am "WIP"

完成開發後,我不是直接執行git merge命令,而是這樣:

git checkout master
git merge --squash private_feature_branch
git commit -v

然後我會花一分鐘時間寫個詳細的commit日誌。

2)較大的工作

有時候一個功能可以延續好幾天,伴有大量的小的commit。

我認為這些改變應該被分解為一些更小粒度的變更,所以squash作為工具來說就有點兒太糙了。(根據經驗我一般會問,“這樣能讓閱讀程式碼更容易嗎?”)

如果我的checkpoint commits之後有合理的更新,我可以使用rebase的互動模式。

互動模式很強大。你可以用它來編輯、分解、重新排序、合併以前的commit。

在我的功能分支上:

git rebase --interactive master

然後會開啟一個編輯器,裡邊是commit列表。每一行上依次是,要執行的操作、commit的SHA1值、當前commit的註釋。並且提供了包含所有可用命令列表的圖例。

預設情況下,每個commit的操作都是“pick”,即不會修改commit。

pick ccd6e62 Work on back button
pick 1c83feb Bug fixes
pick f9d0c33 Start work on toolbar

我把第二行修改為“squash”,這樣第二個commit就會合併到第一個上去。

pick ccd6e62 Work on back button
squash 1c83feb Bug fixes
pick f9d0c33 Start work on toolbar

儲存並退出,會彈出一個新的編輯器視窗,讓我為本次合併commit做註釋。就這樣。

3)捨棄分支

也許我的功能分支已經存在了很久很久,我不得不將好幾個分支合併進這個功能分支中,以便當我寫程式碼時這個分支是足夠新的的。版本歷史讓人費解。最簡單的辦法是建立一個新的分支。

git checkout master
git checkout -b cleaned_up_branch
git merge --squash private_feature_branch
git reset

現在,我就有了一個包含我所有修改且不含之前分支歷史的工作目錄。這樣我就可以手動新增和commit我的變更了。

總結

如果你在與Git的預設設定背道而馳,先問問為什麼。

將公共分支歷史看做不可變的、原子性的、容易追溯的。將私有分支歷史看做一次性的、可編輯的。 推薦的工作流程是:

  1. 基於公共分支建立一個私有分支。
  2. 經常向這個私有分支commit程式碼。
  3. 一旦你的程式碼完善了,就清理掉下私有分支的歷史。
  4. 將乾淨的私有分支merge到公共分支中。

原文地址:http://sandofsky.com/blog/git-workflow.html

作者:@sandofsky

譯者:@candyhorse

相關文章