你知道 Git 嗎?當然知道!今天我想跟你分享幾個我最喜歡的 Git 指令。當我還是初階工程師的時候,我的技術主管教了我其中大多數指令(如果你正在看這篇,嗨 Wojtek 👋),後來我也開始把同樣的指令教給我自己的初階與中階工程師。

其實這篇文章我已經想寫好幾個月了。不過你知道的,這類內容很經典,還需要花點工夫;同時我又一直冒出其他點子,所以就一直延後。最後真正啟發我動手寫的,是 @francistrdev 最近發表了他自己的 Git 指令文章。

不過我的文章會有點不一樣。如果你完全是 Git 新手,還在學基礎,建議先看看 Francis 的 Git Gud!。但如果 addcommitpullpush 對你來說已經是本能反應,那這篇就是為你寫的。

這些可以說是中階程度的指令。很實用。是你在工作中真的會需要的那種,但還不到那種資深級 Git 魔法,例如:

在終端機輸入三行文字,然後你的版本庫裡就會出現一艘太空船。

如果你也有其他這種等級、很實用的指令,歡迎在留言分享!

順帶一提,如果你有興趣,也可以在 Instagram 上追蹤我!我打算在那裡發一些大概可以很浮誇地稱為「開發者生活」的內容。😅 例如今年秋天我有四場研討會,所以你可以期待一些我在歐洲小旅行的幕後照片。我目前那邊的追蹤者還很少,而且我也完全不知道這個實驗最後會變成什麼樣子,不過先放上來給你:@sylwia.lask

好,開始吧。

1. 與 2. git merge vs git rebase

哈哈,第一點就直接有兩個指令。之所以把它們放在一起,是因為很多工程師,即使是有經驗的人,也幾乎把 mergerebase 當成可以互換使用。

當然,它們高層次的目的很像:你想把一個分支的歷史整合到另一個分支。很多時候這代表把你的功能分支更新成包含 maindevelop 或其他功能分支的變更。

但它們的做法其實很不一樣。想像你有這樣的歷史:

A---B---C main
     \
      D---E feature

你一直在 feature 分支上愉快地開發,而別人已經在 main 上新增了提交 C。現在你想把那些變更帶進你的分支。

Merge

如果你在 feature 上執行:

git merge main

Git 會把兩邊的歷史合併起來。通常結果會像這樣:

A---B---C------M
     \        /
      D---E---

M 是一個 merge commit。

原本的歷史會完整保留。Git 會記得你的功能分支是從 main 分岔出去的,兩邊各自發展了一段時間,最後再合併回來。

這樣做的好處是能保留真實發生過的歷史。缺點是,如果你一再把 main 合併進功能分支,歷史很快就可能變得像義大利麵一樣亂。

Rebase

如果改成執行:

git rebase main

Git 會把提交 DE 暫時拿掉,把你的分支移到最新的 main 上,然後再把你的提交重新套上去。

結果會變成:

A---B---C---D'---E'

乾淨很多。

但注意那個撇號。D'E' 在技術上不是原本的 DE。Git 會建立新的提交,產生新的 hash。也就是說,rebase 會重寫歷史

所以一個很好用的經驗法則是:

重寫你自己的工作。對共享歷史的 rebase 要小心。

如果你是獨立在某個功能分支上工作,通常 rebase 是完全沒問題的。如果有三個其他工程師已經基於你的提交在開發,去 rebase 那些提交,可能會讓大家的生活變得相當「精彩」。而且不是好事。

那衝突怎麼辦?

merge 和 rebase 都可能產生衝突。

merge 的時候,Git 會試著在一次操作中把兩條歷史合在一起。你解決衝突檔案後,將它們加入暫存區:

git add .

接著繼續:

git merge --continue

rebase 就比較煩一點,因為 Git 會一個提交、一個提交地重新套用。

這代表你可能先解掉一個衝突,繼續 rebase...

git add .
git rebase --continue

...然後下一個提交又冒出另一個衝突。再一個。再一個。這時你就會開始懷疑所有把你帶到這裡的職涯選擇。😅

但也有一個小小的好處。如果你整個搞砸了,只想把這場鬧劇立刻停下來,可以用:

git merge --abort

或:

git rebase --abort

Git 會盡量把你恢復到 merge 或 rebase 開始前的狀態。非常實用的功能。😁


3. git commit --amend

這是我身為初階工程師時,很快就愛上的指令之一。想像你剛剛建立了一個漂亮、完美的小提交。然後你看了程式碼,發現你漏了一些東西。以我的經驗,通常會是:

console.log("WTF");

這時你有兩種選擇。第一種:再做一個光榮的提交:

Remove console log

老實說,這在 Git 歷史美感上並不算多漂亮。

或者,你也可以直接把漏掉的變更加到前一個提交裡。例如:

git add forgotten-file.ts
git commit --amend --no-edit

--no-edit 的意思是:

把這些變更加到前一個提交,但保留原本的提交訊息。

如果你也想修改提交訊息,直接執行:

git commit --amend

Git 會打開你設定好的編輯器讓你修改。

這裡有一個很重要的細節。amend 不是真的去修改既有提交本身。它會建立一個新的提交,內容是更新後的狀態。由於提交 hash 取決於提交的內容與中繼資料,所以 hash 會改變。

如果你只是在本機操作,還沒 push 到任何遠端,沒問題。但如果這個分支已經存在於遠端,你本機的歷史就不再和遠端歷史一致。這時你如果嘗試:

git push

很可能會看到像這樣的訊息:

rejected (non-fast-forward)

如果你很確定沒有人在使用那個分支,技術上你可以這樣做:

git push --force

但如果你不確定呢?

這就很自然地帶我們進入下一點。


4. git push --force-with-lease

在 rebase、amend、互動式 rebase、squash,或任何會重寫提交的操作之後,你本機的分支歷史可能已經和遠端儲存的版本不一致了。

你可以用這個解決:

git push --force

--force 基本上是在說:

我的本機版本才是正確的。把遠端上的內容全部換成這個。

如果有人在這段時間對同一個分支又推了東西,你可能會把別人的工作覆蓋掉。對方大概會很火大,而且也很合理。😏

更安全的選擇是:

git push --force-with-lease

簡單來說,這是在告訴 Git:

幫我強制推送,但前提是遠端分支還長得跟我預期的一樣。

如果遠端分支在你本機 Git 知道的狀態之後有變動,例如別人推了新提交,Git 會拒絕這次推送,而不是盲目覆蓋。

所以與其毀掉別人的下午,不如得到一個錯誤訊息。我通常會建議:

--force-with-lease

在可行的情況下,優先於:

--force

5. git rebase -i

對我來說,這是非常重要的一個指令。幾乎可以說是 Git 的王者。xDDDD 互動式 rebase 幾乎能讓你對最近的提交做任何想做的事。

換句話說,你可以把每一行都各自提交、改來改去十次,做出像這樣的提交歷史:

Add validation
Fix validation
Actually fix validation
Fix validation again
Remove console.log
Please work now

然後之後再把整堆混亂整理成優雅的 Git 歷史。

假設你想編輯最近五個提交。你執行:

git rebase -i HEAD~5

Git 會打開你設定的編輯器——Vim、Nano、VS Code,或你習慣的任何工具——然後顯示類似這樣的內容:

pick a111111 Add login form
pick b222222 Add validation
pick c333333 Fix typo
pick d444444 Fix validation
pick e555555 Remove debug log

接下來就是好玩的地方了。你可以把 pick 換成其他指令。

pick

保持這個提交原樣不變。

pick a111111 Add login form

reword

保留提交內容,但修改訊息。

reword a111111 Add beautiful login form

edit

在那個提交暫停 rebase,讓你修改它。如果你想改舊提交的實際內容,這很有用。

squash

把這個提交和前一個提交合併。

例如:

pick   a111111 Add login form
squash b222222 Add validation
squash c333333 Fix validation

Git 會把這些提交合成一個,然後讓你編輯最後的提交訊息。

所以你原本可能會有:

Add login form
Add validation
Fix validation

最後可以變成:

Add login form with validation

漂亮。圓潤。完美。

fixup

fixup 類似 squash,但它會丟掉 fixup 那個提交的訊息。

例如:

pick  a111111 Add login form
fixup b222222 Fix typo
fixup c333333 Remove console.log

你大概不太需要保留這種歷史意義:

Remove console.log

所以 fixup 在這裡非常適合。

drop

完全移除這個提交。

drop c333333 Terrible idea

再見。

當然,對於該不該整理提交歷史,大家有不同的流派。有些資深工程師認為,提交一份整理乾淨的歷史給審查是基本禮貌。也有人完全不在意,覺得那沒必要,因為反正 GitHub、GitLab 或你使用的平台在合併 PR 時可以直接 squash 掉全部提交。

但如果你不想按下 Squash and merge 呢?如果你的功能本來就邏輯上有兩個提交,而且你真的想保留它們呢?這種情況是存在的。

所以不管你是一週用一次互動式 rebase,還是半年才用一次,我還是覺得值得認識這個王者:

git rebase -i

6. git stash

這個指令真的常常很好用。你開始做一個不錯的小功能。或者現在這年代,可能是 Kiro 或 Claude Code 在努力工作,而你在旁邊看著。突然有人回報了一個 production bug。

很不幸地,你現在必須暫時放下你美好的功能,切去處理別的事情。問題是你目前的工作根本還沒完成。到處都有除錯 log,半數檔案都被修改了,專案甚至根本建置不起來,而且你絕對不想把這堆亂七八糟的東西 commit 進去。

當然,現在你已經知道 amend 了,之後其實可以慢慢整理。

但有更好的方法:

git stash

Git 會暫時把你尚未提交的變更存起來,並把工作目錄恢復成乾淨狀態。

這時你就可以切換分支:

git switch main

先修好你的 production 災難,之後再回來。

有一個重要細節:預設情況下,git stash 會存追蹤中的檔案,但不會存新的未追蹤檔案。如果你也想把新建立的檔案一起存起來,請使用:

git stash -u

或者更好一點,替 stash 取個有意義的名字:

git stash push -u -m "WIP login feature"

因為一旦你有好幾個都差不多叫「WIP」的 stash,未來的你會討厭現在的你。

git stash list

查看所有 stash:

git stash list

你可能會看到:

stash@{0}: On feature/login: WIP login feature
stash@{1}: On feature/cart: experiment

git stash show

查看某個 stash 內容:

git stash show stash@{0}

如果你想看完整 diff:

git stash show -p stash@{0}

git stash apply

還原某個 stash:

git stash apply stash@{0}

變更會被還原,但 stash 仍會留在清單裡。

git stash pop

你也可以用:

git stash pop

這會套用 stash,並且在成功時把它從 stash 清單中移除。

所以大致上:

apply = 還原
pop   = 還原 + 從 stash 移除

非常簡單,非常實用。


7. git cherry-pick

我得承認,這是我最常使用的中階 Git 指令之一。想像在另一個分支上有一個你現在分支非常需要的提交。

你不想整個分支 merge 過來。你不想 rebase 到那個分支。你只需要那一個東西。例如最近在我的專案裡,我需要一個包含新建環境設定的提交。這就是完美的使用情境。

你只要執行:

git cherry-pick <commit-hash>

例如:

git cherry-pick a1b2c3d

Git 會把那個提交帶來的變更套用到你目前的分支上。

重要細節:它會建立一個新的提交。所以結果提交基本上包含相同的變更,但會有新的 hash。想像這段歷史:

main
A---B---C

feature
     \
      D---E---F

你在 main 上,但你只想要 E

執行:

git cherry-pick E

之後你會得到像這樣的結果:

A---B---C---E'

很簡單。

不過老實說,身為一個有點懶的生物,我也會以稍微沒那麼高尚的方式使用 cherry-pick。有時候我的分支會完全亂掉。rebase 出問題、歷史看起來很可疑、衝突開始倍增,過了一陣子我就會決定:我要在樹頂上從正確的位置開一個全新的分支。😅

而且因為——多虧前面那些指令 ☺️——我的提交已經整理得又漂亮又工整,我就直接一個一個 cherry-pick 到新分支上。

我相信現在一定有某位 Git 高手正在搖頭,但讓我這麼說吧:對我來說,它就是管用。😀


8. git reset --soft--mixed--hard

三種把歷史往回退的方法,每種對這個問題的回答都不同:

我的變更該怎麼辦?

因為我們有多少次是開始做某件事、寫了一些程式,然後突然發現:不對。整個想法都不行。回去吧。

理解 reset 最簡單的方法,是把它想成三層:

提交歷史
暫存區
工作目錄

現在我們有三種越來越激烈的 reset 層級。每往下一層,如果你用錯,心臟都更有機會小小地受驚一下。

git reset --soft

假設你想撤銷最後一個提交:

git reset --soft HEAD~1

Git 會把 HEAD 往回移一個提交,但被移除的提交所包含的變更會保留在暫存區

所以如果你的歷史是:

A---B

在 reset 之後,你的分支會指向:

A

B 引入的變更仍然是可以直接再次提交的狀態。

這在你太早 commit、想用不同方式重做那個提交時很有用。

git reset --mixed

現在:

git reset --mixed HEAD~1

或者直接:

git reset HEAD~1

因為 --mixed 是預設值。

Git 會再往回移一個提交。

但這次變更會保留在你的工作目錄中,而且是未暫存狀態。

所以檔案內容不會消失,但你需要重新 git add 再提交。

git reset --hard

現在進入危險區:

git reset --hard HEAD~1

這會把 HEAD 往回移,並更新暫存區和工作目錄,使其與該提交一致。換句話說,檔案裡的變更也會一起消失。

所以:

--soft  → 保留已暫存變更
--mixed → 保留未暫存變更
--hard  → 捨棄工作樹中的變更

使用最後一個時,請清楚知道自己在做什麼。但如果你一不小心刪掉了完全不該刪的東西呢?

這就帶我們來到...


9. git reflog

這是一個我很少使用的指令。但它救過我無數次。

例如,有一次我執行了 git reset --hard,結果不是只刪掉最後的變更,而是把我已經工作兩天的分支整個弄沒了。😀 這是非常「精彩」的經驗。強烈推薦。

重點是要理解:git log 顯示的是你目前所看的歷史中可達到的提交。如果你把分支往回 reset,某些提交可能就不再出現在 git log 裡。

但這不代表 Git 立刻把它們刪掉了。Git 也會保留像 HEAD 這種參照的本機移動紀錄。你可以用這個查看:

git reflog

你可能會看到像這樣:

e35fa12 HEAD@{0}: reset: moving to HEAD~2
821cd77 HEAD@{1}: commit: Add authentication
f992ab1 HEAD@{2}: commit: Add login page

啊哈!你消失的提交在這裡。

這時你有幾種選擇。你可以把分支直接移回去:

git reset --hard 821cd77

但老實說,如果我已經進入「災難復原」模式,我比較喜歡先做更安全的事:

git branch rescue 821cd77

現在這個提交又可以從一個分支被找到,我也可以冷靜地檢查到底發生了什麼,而不用立刻再去改寫其他東西。

關鍵差異是:

git log

顯示你看得到的提交歷史。

git reflog

顯示你本機的參照,特別是 HEAD,最近指向過哪裡。

不過這裡有一個重要限制。reflog 不是你打過每一個字元的神奇備份。如果你的變更從來沒有被 commit、stash,或以其他方式存成 Git 物件,reflog 也不可能把它們憑空復活。

所以是的,如果你六個小時都沒 commit,然後把那些變更弄壞了...嗯。也許這會教你下次多 commit 一點。


10. git revert

最後是:

git revert

想像你把某個提交發布到 production。或者,在沒那麼硬核的版本裡,發布到某個共享的 develop 分支。

結果出大事了。全部壞掉。那現在怎麼辦?你要對 production 分支執行:

git reset --hard

嗎?

你要開始做驅魔儀式嗎?

幸好不用。要在保留 repository 歷史清楚紀錄的前提下撤銷某個提交,優雅的做法是:

git revert <commit-hash>

假設你的歷史長這樣:

A---B---C

C 引發了災難。

你執行:

git revert C

Git 不會把 C 刪掉。

相反地,它會建立一個新提交,套用相反的變更:

A---B---C---D

你可能會得到像這樣的內容:

C: Add new payment logic
D: Revert "Add new payment logic"

這在共享分支上非常有用,因為你不需要重寫公開歷史。大家都能清楚看到:

  1. 原本的變更發生了,
  2. 它造成了問題,
  3. 它被回滾了。

這和把分支 reset 後再強制推送回去相比,後者會重寫歷史,還可能讓其他協作者出問題。

所以,一般來說:

共享分支 + 壞掉的提交 → revert

通常比:

reset + force push

安全得多。

就這樣!

就像我一開始說的,如果你還有其他中階 Git 指令,能經常幫你省時間,或救你一命,歡迎在留言分享。通常我文章底下的留言最後都會比文章本身更有料,這件事總是讓我很開心。

另外,也請注意,上週的文章是一篇清單。這週的文章也是一篇清單。而且——先劇透一下——下週的文章也還會是一篇清單!

這不是因為我聞到流量味道,決定變成 BuzzFeed。只是剛好就這樣發生了。😅

希望你學到一些新東西。就算沒有,也希望至少讀起來有趣。😁


原文出處:https://dev.to/sylwia-lask/10-git-commands-youll-wish-you-knew-earlier-4fcp


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬4  
178
🥈
我愛JS
💬1  
6
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登