Git 版本控制 › 第 10 章

實用工具與工作流

2026-07-02  ·  stash · cherry-pick · tag · amend · Git Flow · GitHub Flow · Trunk-based

前九章建立了完整的核心心智模型:物件、指標、分支、合併、遠端、悔改機制。本章收尾兩件事—— 幾個高頻率用到的實用指令(stashcherry-picktagcommit --amend), 以及把這些指令串起來的團隊工作流。工作流不是新指令,而是「大家約定好怎麼用分支」的協定—— 8.4 已經講過 push/pull/PR 的機制,這裡要拉高一層,看整個團隊怎麼分工。

10.1git stash:暫存還沒做完的修改

有時你正在改一個檔案,還沒到能 commit 的程度,卻必須立刻切去處理別的事(緊急 bug、切分支 review 別人的 PR)。 直接切分支,未 commit 的修改會跟著帶過去,容易搞混。git stash 把「工作目錄 + 暫存區的修改」 整包收進一個獨立的堆疊,工作目錄瞬間變乾淨,之後隨時可以取回。

stash 的本質是把目前的修改包成一組類似 commit 的物件(不進分支歷史,獨立存在 refs/stash), 所以它能利用 Git 的物件模型:安全、可 diff、可挑著套用——概念上跟 2.4 節的 commit 很像,只是不掛在任何分支上。
情境:main 上正在改 search.js(加入搜尋防抖),還沒 commit,忽然要處理另一件事。點按鈕模擬操作:
🖥 工作目錄
search.js:新增搜尋防抖(debounce),尚未 commit
📥 stash 堆疊(git stash list)
目前是空的
點選上方任一按鈕,觀察工作目錄與 stash 堆疊的變化。
指令效果
git stash push(或省略 push 直接 git stash)把目前修改推進堆疊最上層(stash@{0}),工作目錄回復乾淨
git stash list列出堆疊裡所有項目,由新到舊
git stash pop取出最上層一筆、套用回工作目錄,並從堆疊移除(= apply + drop)
git stash apply套用最上層一筆,但保留在堆疊裡——想把同一份修改套到多個分支時用這個
git stash drop直接丟棄最上層一筆,不套用——確定不要了才用
套用(pop/apply)時如果工作目錄本身已經有別的修改,兩邊可能產生衝突,標記方式與 6.2 節的合併衝突相同—— 解法也一樣:找到 <<<<<<< 標記、決定保留內容、git add 標記解決。

10.2git cherry-pick:只挑一筆 commit 過來

merge/rebase 是把「整條分支」的變更帶過來;cherry-pick 則是精準挑選 某一筆 commit,把它的變更內容複製到目前分支,成為一個全新的 commit。 典型情境:某個 hotfix 先在 feature 分支修好了,但你不想把整條 feature 分支合併進來,只想要那一筆修正。

cherry-pick 示意圖 feature 分支上的 commit X 被 cherry-pick 到 main,在 main 上產生內容相同但 SHA 不同的新 commit X' feature A B X 要挑走的那一筆 C main D E cherry-pick X X' ▸ 點我看為何 SHA 不同 main
圖 10-1cherry-pick 把 feature 上的 X 複製成 main 上的新 commit X'

cherry-pick 很常撞衝突,而且原因跟 7.2 節的 rebase 是同一個:X 這筆 commit 是寫在 feature 的脈絡上的, Git 要把它套到 main 上時,本質是「貼一塊 patch」——X 改的那幾行,在 main 上可能早就長得不一樣,甚至還不存在。 挑一筆 commit 到差異越大的分支,patch 對不上的機率就越高,而這正是 cherry-pick 最典型的使用情境。

撞到衝突時 cherry-pick 會停在原地,處理方式跟 6.2 節的合併衝突一模一樣:檔案裡同樣是 <<<<<<< 標記,同樣是「挑內容、刪標記、git add 標記已解」。 解完之後用 git cherry-pick --continue 讓 Git 把 X' 建出來;中途反悔就 git cherry-pick --abort, 回到 cherry-pick 之前的狀態,什麼都不會少——這組「解完 --continue、放棄 --abort」的節奏, 跟 7.3 節的 rebase 是同一套,認得一次就到處都通。
如果之後又把整條 feature 合併回 main,Git 可能對已經 cherry-pick 過的內容再合併一次而產生衝突或空 diff。 建議 cherry-pick 時加 -x:git cherry-pick -x <sha> 會在新 commit 訊息自動附上 (cherry picked from commit ...),方便日後追查來源、也讓部分工具能識別重複。

10.3git tag:標記一個永久不動的版本點

分支(branch)是會移動的指標——每次 commit 就往前推進。tag 則相反,是釘死在某一點不動的指標, 專門用來標記「這是 v1.2.0」這種有意義的版本節點。Git 有兩種 tag,差別在於它是不是自己的物件。

lightweight tag 與 annotated tag 的物件結構對照 lightweight tag 的 ref 直接指向 commit;annotated tag 的 ref 指向一個獨立的 tag 物件,再由 tag 物件指向 commit lightweight(git tag v1.0-lw) refs/tags/v1.0-lw C commit ref 直接指向 commit annotated(git tag -a v1.0 -m "...") refs/tags/v1.0 tag 物件 ▸ 點我看內容 C
圖 10-2lightweight tag 是純 ref;annotated tag 多一層自己的物件(呼應 2.5 節 .git/refs 的結構)
指令訊息 / tagger / 日期可簽署建議情境
lightweightgit tag v1.0-lw不可暫時性標記、個人本地書籤
annotatedgit tag -a v1.0 -m "發布 1.0"git tag -s正式版本發布(建議預設用這個)

上面兩個範例都沒指定 commit,標籤就打在 HEAD 上。但只要在最後補一個 commit SHA(或分支名等任何 commit-ish), 就能對歷史上任意一筆 commit 補標籤:git tag v1.0-lw <sha>,annotated 也一樣 git tag -a v1.0 -m "發布 1.0" <sha>。發布當下忘了打 tag、事後才想補,靠的就是這招—— tag 只是個指標,指到哪筆 commit 跟你「什麼時候下這個指令」無關。

但事後補 annotated tag 有個小陷阱:tag 物件裡的 tagger 日期記的是「你補標籤的當下」, 而不是那筆 commit 的日期——今天替半年前的發布補 tag,tag 上寫的就是今天。 commit 自己的日期完全不受影響,但用 taggerdate 排序版本、或靠 tag 日期產生 release notes 的工具會看到這個落差。 lightweight tag 沒有這個問題,因為它根本沒存日期(見上面的 tag 物件說明)。
git push 預設不會推送 tag,要另外 git push origin v1.0git push --tags 推全部。git checkout v1.0 會進入 detached HEAD(見 3.3 節)—— tag 本身不能被 commit 在上面移動。

10.4git commit --amend:修改「上一筆」commit

commit 訊息打錯字、忘了加一個檔案、想再補幾行程式碼到剛剛那筆——--amend 讓你不用開一個新 commit,而是用一個新 commit 取代最後一筆。 本質上跟 9.1 節的 reset --soft 加上重新 commit 是同一件事,只是包成一個指令。

commit --amend 前後對照 amend 前 main 指向 C;amend 後 C 變成 dangling,main 改指向擁有相同 parent 的新 commit C' amend 前 A B C main git commit --amend amend 後 A B ⚠ dangling C C' main
圖 10-3amend 前 main 指向 C;amend 後 C 變成 dangling,main 改指向 parent 同樣是 B 的新 commit C'
用法效果
git commit --amend打開編輯器讓你改上一筆的訊息,內容不變
git commit --amend -m "新訊息"直接指定新訊息,不開編輯器
git add 忘記加的檔案 && git commit --amend --no-edit把新暫存的變更併入上一筆,訊息保持不變
C 的 SHA 徹底改變(parent 相同,但 tree/訊息/時間不同),等於改寫了歷史—— 跟 7.3 節「黃金法則」的邊界完全一樣:只 amend 還沒推送、只有自己看得到的 commit。 已經推送、別人可能已經拉走的 commit,amend 之後推送需要 --force,會讓別人的本地歷史與遠端分岔。 真的手滑 amend 錯了東西,C 並沒有消失、只是 dangling——9.3 節的 reflog 一樣能救回來。

10.5團隊工作流(Git Flow / GitHub Flow / Trunk-based)概覽

工作流是團隊對「分支怎麼開、怎麼合、多久發一次版」的共同約定,本質上是把前面九章的指令組合成固定套路。 三種主流工作流的差異,其實就是分支複雜度發布頻率的取捨。點下方分頁切換, 再點節點看每一步對應的指令。

GitHub Flow 流程圖 建立分支、提交變更、開 PR、Code Review、合併並部署,五個步驟 ① 建立分支 ▸ 點我展開 ② 提交變更 ▸ 點我展開 ③ 開 PR ▸ 點我展開 ④ Code Review ▸ 點我展開 ⑤ 合併 &部署
圖 10-4GitHub Flow:只有 main + 短命 feature 分支,合併進 main 即自動部署
工作流分支複雜度release 方式線上出包怎麼修適合情境
Git Flow高(main / develop / feature / release / hotfix 五類)明確版本節點,release 分支收尾、打 tag專屬 hotfix/*:從 main 切出,修完合併回 main 與 develop 兩邊、打 tag有固定發版週期的產品(桌面軟體、正式版雲端服務)
GitHub Flow低(main + 短命 feature)合併進 main 即部署,持續交付沒有特別流程:就是一條短命分支走 PR → 合併 → 部署,快慢看 CI/CD網頁服務、SaaS,想要持續部署
Trunk-based最低(幾乎只有 main)靠 feature flag 控制對外可見,發布與部署解耦同上,再加上 feature flag:關掉出包的功能不必重新部署,常比回滾更快大型團隊、高頻率整合、成熟 CI/CD 文化

這一欄的差別,關鍵不在「誰比較會修 bug」,而在誰的正常路徑夠快:GitHub Flow 與 Trunk-based 之所以不需要專門的 hotfix 流程,是因為它們平常就在做「合併進 main 就部署」——急件走的是同一條路,只是插隊。 Git Flow 得另外開一條 hotfix/*,正是因為它的正常路徑(develop → release → main)慢、main 又離 develop 很遠, hotfix 分支存在的意義就是繞過 Git Flow 自己的慢。代價是「合併回兩邊」這步很容易漏: 只合回 main 沒合回 develop,線上是修好了,但下一版從 develop 發出去時,同一個 bug 會原封不動回來。

沒有「最好」的工作流,只有「適合目前團隊規模與發版節奏」的工作流。多數新專案從 GitHub Flow 起步就夠用; 真的有固定版本週期(如需要同時維護多個舊版本)再考慮 Git Flow 的完整分工。

10.6本章小結

概念一句話記住
stash把未 commit 的修改整包收進獨立堆疊,工作目錄瞬間變乾淨,不建立正式 commit
cherry-pick複製別的分支上某一筆 commit 的內容,在目前分支產生一個全新的 commit(新 SHA)
tag(lightweight)只是一個指向 commit 的 ref,沒有自己的物件
tag(annotated)多一個 tag 物件(訊息 / tagger / 可簽署),ref 指向 tag 物件而非直接指向 commit
commit --amend用新 commit 取代最後一筆(舊的變成 dangling),等同 reset --soft 加上重新 commit
團隊工作流Git Flow(重)/ GitHub Flow(輕)/ Trunk-based(最輕)——依發版頻率與團隊規模選擇
到這裡,Git 版本控制的主線九章 + 本章全部收尾:物件模型(2)→ commit / HEAD / 分支(3)→ 日常操作(4) → 分支合併與衝突(5、6)→ rebase 抉擇(7)→ 遠端協作(8)→ 回溯救援(9)→ 實用工具與工作流(10)。 你現在看到任何 Git 狀態,腦中都該能浮現對應的物件圖或指標圖。若對 diff/patch 的底層原理還有興趣,附錄 A 有更深入的探討。