Git 版控入門算算一年多,自覺還算上手,能善用 Branch、Commit、Reset,要把程式改爛到回不去還真有些難度。而最美妙的是 - Git 屬分散式版控,在自己的機器可以隨意 Commit、Reset、開 Branch ,天王老子都不能管你,只要送上 TFS 或 Github 前先用 Squash、Rebase 好好整理,Push 到伺服器的程式碼整整齊齊,沒人會知道這段程式曾改過十幾次理由還超低級。寫程式時像有張可靠的安全網時時保護你,能放膽嘗試各種構想挑戰超越自己,Coding 就是要這樣才盡興!

推坑時間:如果你有在寫程式卻看不懂上面這一大堆術語?那你一定也不知道自己錯過什麼好東西,大力推薦這個要花點時間學但絕對值回票價的神兵利器 - Git!!

【延伸閱讀】

不過,學習很難一次到位,入門時遺漏的訣竅技巧,要靠實戰經驗補齊。最近對 Git 的 Cherry Pick 有些心得,整理分享給大家參考。

我們都知道,將 Commit 搬到其另一條 Branch,除了 MergeRebase外,還有一個好用功能叫 Cherry-Pick,可以從其他 Branch 單獨挑幾個 Commit 搬到現在的 Branch 上。我有一些小型專案只區分 master 跟 release 兩個 Branch,遇到想先上線部分修改時,用 Cherry-Pick 挑選要上線 Commit 從 master 搬到 release 是簡單又直覺的做法。

不過,使用 Cherry Pick 是有後遺症的,之前看的中文文章很少提到這部分,就由我來補上吧!

使用 Cherry Pick 主要衍生兩個問題,一是搬過去的 Commit 雖然說明訊息與來源相同(如果選擇 Cherry Pick 後手動 Commit,則連說明也不會相同)但二者 Hash 值可能不同,無法建立連結,未來不易追溯完整修改歷程。

第二個問題則是:Cherry Pick 過再做 Merge,會產生重複 Commit 項目。

為了反覆實驗,我寫了一串 DOS 指令重現問題:

rem 清空上次 Git 測試結果
rmdir .git /s /q
del . /q
rem 建立 Git Repository
git init
rem 建立 file1.txt 並 Commit
echo "1234" > file1.txt
git add . && git commit -m "init"
rem 建立 relase Branch
git branch release
rem 在 master 加入 file2.txt 並 Commit
git checkout master
echo "ABCD" > file2.txt
git add .
git commit -m "commit for later merge"
rem 繼續加入 file3.txt 並 Commit
echo "Git Rocks!" > file3.txt
git add .
git commit -m "commit for cherry-pick"
rem 取得 file3.txt 這個 Commit 的 Hash 值稍後 Cherry Pick 要用
for /f %i in ('git rev-parse HEAD') do set hash=%i
rem 切到 release 並 Cherry Pick file3.txt 那個 Commit
git checkout release
git cherry-pick %hash%
rem 回到 master 繼續新增 file4.txt 並 Commit
git checkout master
echo "Hello, World" > file4.txt
git add .
git commit -m "commit for merge"
rem 將 master 合併到 release
git checkout release
git merge master

註:利用 FOR /F 取 Commit Hash 的原理可參見 如何利用批次檔(Batch)讀取指令執行的結果或文字檔案內容 by 保哥

在這個展示中,我開了 master 跟 relese 兩個 Branch,在 master 加了三個 Commit,先 Cherry Pick 第二個 Commit 到 release,之後再將 master Merge 到 realse。

master 歷史如下:

release 有幾個重點,1 所指的 Commit 是用 Cherry Pick 拉過來的,雖然訊息相同,但 Hash ID 586de312 與來源 d0fb85f3 不同。之後 Merge 把 master 的所有更新合進來時,剛才 Cherry Pick 拉進來的 Commit 又再出現一次 (2 所指的地方)。

產生重複 Commit 是預期中的行為,如果要排除的做法一般是 master 先對 release 做 Rebase 再 Merge:

這樣子,master 會調整 Commit 順序向 release 看齊,"commit for cherry-pick" 變成第一個,Hash Id 也從 d0fb85f3 變成 586de312,其餘兩個 Commit 也要重算過,Hash Id 都跟原本不同:

master 都配合到這地步,Merge 到 release 當然順暢無比,連小耳朵都沒有:

不過,如果 master 曾經 Push 到伺服器端,Rebase 會修改歷史帶來困擾,顯然不是個好選項。

查了幾篇英文文章,都提到 Cherry Pick 的負面影響,並強調 Git 社群不推 Cherry Pick:

Cherry picking is commonly discouraged in developer community. The main reason is because it creates a duplicate commit with the same changes and you lose the ability to track the history of the original commit. If you can merge, then you should use that instead of cherry picking. Use it with caution!
在開發社群通常不鼓勵使用Cherry Pick,主要原因是它會產生重複 Commit 且無法追蹤原始修改歷程,如果能 Merge 就別用它,請小心使用。

Even though this feature is interesting and awesome, it has been discouraged in the git community. The main reason is that it creates a duplicate commit with the same changes and you lose the ability to track the history of the original commit.
Also note that if you are cherry-picking a lot of commits out of order, those will be reflected on your branch in the order you cherry picked, not the on the chronological order of the original commits. Sometimes this may lead to undesirable results in your branch
雖然 Cherry Pick 很威,但在 Git 社群並不鼓勵使用,主要原因是它會產生重複的 Commit 且無法追蹤原始修改歷程。Cherry Pick 加入的 Commit 在 Branch 上會以 Cherry Pick 操作時間而非修改發生的時間排序,有時可能導致悲劇。

至此得到結論:

  1. Cherry Pick 之後再 Merge 會產生重複的 Commit
  2. 透過 Cherry Pick 建立的 Commit 將難以追溯完整修改歷程
  3. 如果可以 Merge,就別用 Cherry Pick

好,回到實務上,該如何防止 Cherry Pick 造成的副作用?依本文的 master/release 案例,我想到幾種可行策略:

  1. 採行「release 與 master 脫鉤」政策,讓 release 的 Commit 單純反映每次部署或發行異動,之後 master 到 release 一律用 Cherry Pick,追蹤程式修改歷程由 master 及其他 branch。
  2. 將 master 升級成確認要部署發行才能合併(性質拉高到等同 release),開發人員一律另開 Branch 開發,並三不五時對 master 進行 Rebase,如此即使用 Cherry Pick 拉 Commit 到 master,透過 Rebase 重新對齊,之後開發告一段落要 Merge 回 master 時歷史軌跡完整且不會有 Commit 重複問題。代價是分支變多,管理與合併成本會上升,而各開發人員的本機 Branch,還是要設法 Push 到伺服器端保存,因為常 Rebase 得開放 push -f,或許該跟 master/release 分開管理。
  3. 一言不合就開 Branch。大小修改異動都另起一個 Branch,改好再 Merge 回去,這樣子應該就用不到 Cherry-Pick 了。(雖然開 Branch 很便宜,但數量一多要合併會讓人頭暈吧?)

抛磚引玉丟出這個議題,歡迎大家分享私房祕技。

Explaining the side effect of Git cherry-pick, including duplicated commits and hard to trace history.


Comments

# by abc0922001

以前「pick 後 commit 會重複,要避免使用」 現在「我就重複(比讚」 我用 git extensions 來 cherry pick 時,commit message 會幫我備註來源的 Hash Id,所以我沒有結論2的問題,而且之後 merge 不會 conflict 就好了

# by abc0922001

只要 blame 時能找到兇手,merge 時不會衝突造成疑惑(修改一模一樣的話 git 不會報衝突),我個人不會在意 commit log 好不好看,有沒有重複

# by Jeffrey

to abc0922001, 容忍重複 Commit 的確也是種策略(如果管理規範允許的話), Git Extension 為 Cherry Pick Commit 加上來源 Hash Id 是好點子,有空來研究一下。感謝經驗分享。

# by Eric

User提一堆修改改到一半才說XXX測完了要先過版 遇到這種真的很煩

# by Danny Lin

小專案一般我是採用 master/devel 策略,平時在 devel 開發,部署和發行是一起的(也就是 master 兼 release 的功能),把 devel 整合進 master,然後進版號、上 tag、 deploy、push master to repo。接著把 devel rebase 到 master 上開始下一版本的開發。 - devel 就是草稿,可以一直留在 local 也可以 push to public repo,但就是講明此分支僅供參考,隨時可能改寫。如有其他開發人員要開發新功能,原則上一律 base on master。 - master 只放穩定內容,一般是出新版本時才整條線推上去,偶爾也會 push 一些確定不會動的東西,比如改 README,或是某個下一版確定要加入且會被其他下一版打算加入的功能所依賴的功能(讓其他開發者可以 base on)。萬一很不幸遇到 push 出去的功能打算取消的情況,那就用 git revert 建立反提交。 如果有其他協作者,一般是請他們 fork 和 PR。如果要在同一個 repo 有其他協作者,那就把 public repo 設為只有 master 禁止 force push,大家平時各自使用不同名稱的 devel 開發,master 盡量由固定的專案負責人做更新。

# by Danny Lin

Git 專案本身差不多也是這做法,有個 pu branch 基本上就是像我的 devel 那樣隨時更動,master 原則上出新版本更新。GitHub 上有原始碼,可以參考看看。

# by Jeffrey

to Danny Lin, 感謝寶貴經驗分享。(已筆記)

# by Jeffrey

to Eric,(點頭) 提前上線情境是 Cherry Pick 的主要推手。

# by Stan

如果在 master 另開一個 feature branch 使用 git reset --hard HEAD~n 來整理 commit,只保留原本想要 cherry-pick 的 commit ,再把 feature branch merge into release 不知道是不是好的做法?

Post a comment