2026-07-02 · stash · cherry-pick · tag · amend · Git Flow · GitHub Flow · Trunk-based
stash、cherry-pick、tag、commit --amend),
以及把這些指令串起來的團隊工作流。工作流不是新指令,而是「大家約定好怎麼用分支」的協定——
8.4 已經講過 push/pull/PR 的機制,這裡要拉高一層,看整個團隊怎麼分工。
git stash:暫存還沒做完的修改
有時你正在改一個檔案,還沒到能 commit 的程度,卻必須立刻切去處理別的事(緊急 bug、切分支 review 別人的 PR)。
直接切分支,未 commit 的修改會跟著帶過去,容易搞混。git stash 把「工作目錄 + 暫存區的修改」
整包收進一個獨立的堆疊,工作目錄瞬間變乾淨,之後隨時可以取回。
stash 的本質是把目前的修改包成一組類似 commit 的物件(不進分支歷史,獨立存在 refs/stash),
所以它能利用 Git 的物件模型:安全、可 diff、可挑著套用——概念上跟 2.4 節的 commit 很像,只是不掛在任何分支上。
search.js(加入搜尋防抖),還沒 commit,忽然要處理另一件事。點按鈕模擬操作:
| 指令 | 效果 |
|---|---|
git stash push(或省略 push 直接 git stash) | 把目前修改推進堆疊最上層(stash@{0}),工作目錄回復乾淨 |
git stash list | 列出堆疊裡所有項目,由新到舊 |
git stash pop | 取出最上層一筆、套用回工作目錄,並從堆疊移除(= apply + drop) |
git stash apply | 套用最上層一筆,但保留在堆疊裡——想把同一份修改套到多個分支時用這個 |
git stash drop | 直接丟棄最上層一筆,不套用——確定不要了才用 |
<<<<<<< 標記、決定保留內容、git add 標記解決。
git cherry-pick:只挑一筆 commit 過來
merge/rebase 是把「整條分支」的變更帶過來;cherry-pick 則是精準挑選
某一筆 commit,把它的變更內容複製到目前分支,成為一個全新的 commit。
典型情境:某個 hotfix 先在 feature 分支修好了,但你不想把整條 feature 分支合併進來,只想要那一筆修正。
cherry-pick 很常撞衝突,而且原因跟 7.2 節的 rebase 是同一個:X 這筆 commit 是寫在 feature 的脈絡上的, Git 要把它套到 main 上時,本質是「貼一塊 patch」——X 改的那幾行,在 main 上可能早就長得不一樣,甚至還不存在。 挑一筆 commit 到差異越大的分支,patch 對不上的機率就越高,而這正是 cherry-pick 最典型的使用情境。
<<<<<<< 標記,同樣是「挑內容、刪標記、git add 標記已解」。
解完之後用 git cherry-pick --continue 讓 Git 把 X' 建出來;中途反悔就 git cherry-pick --abort,
回到 cherry-pick 之前的狀態,什麼都不會少——這組「解完 --continue、放棄 --abort」的節奏,
跟 7.3 節的 rebase 是同一套,認得一次就到處都通。
-x:git cherry-pick -x <sha> 會在新 commit 訊息自動附上
(cherry picked from commit ...),方便日後追查來源、也讓部分工具能識別重複。
git tag:標記一個永久不動的版本點分支(branch)是會移動的指標——每次 commit 就往前推進。tag 則相反,是釘死在某一點不動的指標, 專門用來標記「這是 v1.2.0」這種有意義的版本節點。Git 有兩種 tag,差別在於它是不是自己的物件。
.git/refs 的結構)| 指令 | 訊息 / tagger / 日期 | 可簽署 | 建議情境 | |
|---|---|---|---|---|
| lightweight | git tag v1.0-lw | 無 | 不可 | 暫時性標記、個人本地書籤 |
| annotated | git 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 跟你「什麼時候下這個指令」無關。
taggerdate 排序版本、或靠 tag 日期產生 release notes 的工具會看到這個落差。
lightweight tag 沒有這個問題,因為它根本沒存日期(見上面的 tag 物件說明)。
git push 預設不會推送 tag,要另外 git push origin v1.0
或 git push --tags 推全部。git checkout v1.0 會進入 detached HEAD(見 3.3 節)——
tag 本身不能被 commit 在上面移動。
git commit --amend:修改「上一筆」commit
commit 訊息打錯字、忘了加一個檔案、想再補幾行程式碼到剛剛那筆——--amend
讓你不用開一個新 commit,而是用一個新 commit 取代最後一筆。
本質上跟 9.1 節的 reset --soft 加上重新 commit 是同一件事,只是包成一個指令。
| 用法 | 效果 |
|---|---|
git commit --amend | 打開編輯器讓你改上一筆的訊息,內容不變 |
git commit --amend -m "新訊息" | 直接指定新訊息,不開編輯器 |
git add 忘記加的檔案 && git commit --amend --no-edit | 把新暫存的變更併入上一筆,訊息保持不變 |
--force,會讓別人的本地歷史與遠端分岔。
真的手滑 amend 錯了東西,C 並沒有消失、只是 dangling——9.3 節的 reflog 一樣能救回來。
工作流是團隊對「分支怎麼開、怎麼合、多久發一次版」的共同約定,本質上是把前面九章的指令組合成固定套路。 三種主流工作流的差異,其實就是分支複雜度與發布頻率的取捨。點下方分頁切換, 再點節點看每一步對應的指令。
典型流程:main 建立 develop → feature/* 從 develop 切出、完成後合併回 develop
→ 準備發版時從 develop 切出 release/*,收尾後合併回 main 與 develop、main 上打 tag
→ 上線後如需緊急修復,從 main 切出 hotfix/*,完成後同樣合併回 main 與 develop、打 tag。
| 工作流 | 分支複雜度 | 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 會原封不動回來。
| 概念 | 一句話記住 |
|---|---|
| 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(最輕)——依發版頻率與團隊規模選擇 |
diff/patch
的底層原理還有興趣,附錄 A 有更深入的探討。