理解Git的工作流程

張 重騏發表於2013-04-01

如果你不理解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 merge命令,而是這樣:

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

2)較大的工作

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

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

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

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

在我的功能分支上:

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

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

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

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

捨棄分支

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

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

總結

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

將公共分支歷史看做不可變的、原子性的、容易追溯的。將私有分支歷史看做一次性的、可編輯的。

推薦的工作流程是:

本文由張重騏(@candyhorse)投稿於伯樂線上,也歡迎其他朋友投稿。提示:投稿時記得留下微博賬號哦 :-)

相關文章