Git 操作筆記
用 Git 來做版本管理時,偶爾會需要撤銷某些操作,但對於已經推上遠端 Github / Gitlab repository 的程式碼,並且該分支已經有多人協作的時候,如果要修正的話其實有蠻多需要注意的事情呢 ; 再來 Git 是有蠻多功能和一些命令的,有時不常用的話也會忘記或者錯估使用場景。在開始筆記之前先把一些名詞定義好,一般來說 Git 的操作會涉及到幾個區域 :
- 硬碟區 disk : 檔案存放的一般資料夾,也會被稱為 workspace
- 暫存區 staging : 保存
git add紀錄的地方,也被稱為 index- 本地端 git local : 保存
git commit紀錄的地方,也被稱為 repository- 遠端 git remote :
git push的倉儲,如 Github、Gitlab 等等而各個之間狀態變化的簡單關係如下圖所示 :
筆記事項
revert
git revert 和 git reset 都是 Git 撤消 commit 的一種形式,但是 revert 和 reset 的不同點是 git revert 是會增加一個反操作的 commit :
那麼和 reset 這個相對直觀的命令相比 revert 有什麼好處呢 ?
revert可以撤銷中間任意一個 commit
比如下圖有一個 Change commit 後又有一個 Change2 commit , 假設這個 Change commit 的 id 是 70a2..:
git revert 70a2 這樣最終得到的結果,就相當於只做了一個 Change2 ,而這個效果是我們用 reset 沒有辦法輕鬆地達到的。還有一個更重要的好處,是我們 push 到了遠端之後,由於對於公有分支 commit 是只能往前走,不可以刪除和更改的,故不可以進行 git reset 只能使用 git revert
如果我們修改的分支是除了自己之外沒有任何其他人用,就可以用 git reset 把某些歷史 commit 直接砍掉,但注意這個時候想同步到遠端的話必須使用 git push -f 強制推送更改,因為遠端的 歷史 commit 可能和自己本地端不一樣。
對於「公有分支」絕對不應該使用 git push -f ; 但是如果這是「個人分支」的話其實是可以使用 git push -f。雖然是這樣說,也是有公司希望就算是自己的 branch 也完全不要使用 git push -f 的,這些規範建議都先問一下比較好。
「直接 Merge」 VS 「Rebase 後才 Merge」
假設情境(合併前)
D---E master
/
A---B---C---F origin/master
直接使用 merge
D--------E
/ \
A---B---C---F----G master, origin/master
# G 為 merge commit,記錄了 D–E 和 F 的合併狀態。
使用 rebase 後才 merge
A---B---C---F---D^---E^ master, origin/master
# D^ 和 E^ 內容其實和 D 和 E 是一樣的,只是 commit SHA 不同。
為什麼 commit SHA 會改變呢 ? 因為 git commit 生成的 SHA 會依賴前一個 commit ,由於 rebase 導致依賴的前一個 commit 和原本的不一樣,所以 commit SHA 才會改變。
表格整理
| 項目 | merge | rebase 後才 merge |
|---|---|---|
| 歷史紀錄 | 保留完整分支脈絡,會多一個 merge commit | 重寫歷史,變成線性結構 |
| Commit SHA | 不變 | 會改變(如 D → D’) |
| 衝突處理次數 | 只需處理一次 | 每個 commit 都可能衝突,要多次處理 |
| 操作難度 | 相對簡單 | 需要每步都手動確認,再來 git rebase --continue |
| 推薦使用時機 | 大範圍修改且預期有大 conflict | 想保持線性紀錄 |
特別注意,是在新的 branch 上使用 rebase ,而不是在 main/master 上使用 rebase,因為 rebase 其實算是一個「危險」的操作,會改寫 commit 的歷史,實務上不會對團隊共同開發的穩定分支(像 master, main, dev,…等)下這個指令
偶爾出現的 origin 是什麼
在 Git 中,origin 是預設的遠端名稱,代表 clone 時的來源,有些 cli 會加上 origin 是為了明確指定要從哪個 remote 抓取資料,因為可能有多個遠端如 origin、upstream 等等 :
# 用來查看目前專案所設定的 remote repository
git remote -v
# Terminal:
# origin git@github.com:Aryido/aryido.github.io.git (fetch)
# origin git@github.com:Aryido/aryido.github.io.git (push)
# 目前只有一個 origin 遠端
那什麼情況下會有多個遠端倉庫呢?
同步多個平台的倉庫,例如 GitHub 和 GitLab
Fork 開源專案:origin 與 upstream
當從 GitHub 上 fork 一個開源專案時,預設的遠端名稱為 origin 會指向自己的 fork。但為了同步原始專案的更新,可以新增一個名為 upstream 的遠端,指向原始專案的倉庫。這樣可以方便地從原始專案拉取更新,然後是把更改推送到自己的 fork。
fetch & pull 差異
fetch
# 從預設 remote(origin)抓取所有分支更新
git fetch
git fetch origin
# 從指定的 remote(如 origin)抓下特定 <branch> 更新
git fetch origin <branch>
# 對於所有有設定的 remote ,抓取所有分支的更新,例如同時抓 origin 和 upstream 的所有 branch 更新
git fetch --all
特別注意 git fetch 只是下載所需的資料,不會合併和更新任何的檔案
pull
# 預設會根據 origin 更新本地的所有 branch
git fetch
# 預設會根據 origin 只更新本地的 master
git fetch origin master
# 但以上兩個做完,在 master 上的資料都還沒更新,只有把資料下載下來而已
# 現在才手動把更新合併進來
git merge origin/master
---
# 等同於上面兩步:fetch + merge
git pull
origin master 還是 origin/master ? 何時使用斜線 /
當從 remote 抓取分支時,Git 會在本地建立對應的遠端追蹤分支,其命名格式為: <remote>/<branch>
例如: origin/master 表示遠端 origin 的 master 分支在本地的追蹤分支:
# 會使用斜線 / , 表示將遠端 origin 的 master 分支合併到當前本地分支
git merge origin/master
在某些命令中,你需要分別指定遠端名稱和分支名稱作為參數,此時不使用斜線 /:
# 表示從遠端 origin 抓取 master 分支的更新
git fetch origin master
reflog
reflog 是縮寫,全稱是 reference logs
git log: commits 的紀錄git reflog: 使用者在 git 上的操作紀錄
使用場景: 例如用了 git reset 語法,將版本還原到太前面的版本,用 git log 上看不到那些比較新的 commit ,此時想回復到原來狀態該怎麼辦?此時可以用 git reflog 指令,它會詳細顯示你每個指令的 SHA-1 值。
情境
1. 假設我對 file 進行了一些修改,但我發現對 file 的修改是錯誤或不必要的,所以我想測銷掉對 file 的修改
# 沒有 `git add` 也沒有 `git commit`
git checkout < changed_file >
git restore < changed_file >
# 以上兩個操作等價
# 由於 checkout 還有切換 branch 的功能,新版本 git 想把這兩件事分開所以有了 restore
# 但我個人沒用過 restore,就簡單筆記一下
# 使用了 `git add` 但還沒 `git commit`, 反悔了想取消 `git add`
git reset < changed_file >
git restore --staged < changed_file >
# 上面的操作兩個等價且安全,只會把文件從 staging 移出,會保留在 workspace 上的修改
# 個人也沒用過這個 restore,也簡單筆記一下
# 如果就是想測銷掉所有改動,且直接恢復到原始狀態,可以使用
git checkout HEAD < changed_file >
# HEAD 代表 git 內最近一次的 commit
2. 使用了 git commit 產生了一個叫 change 的 commit 了,但是想取消
假設在初始狀態下 disk, staging, local, remote 這四個是保持同步的,再來假設初始狀態下已經有一個 commit 叫做 init ,然後又 commit 一個叫 change 的 commit,如果要取消這個 commit 的話:
# 以下兩個等價,reset 的預設就是 --mixed
git reset HEAD~1
git reset --mixed HEAD~1
由於產生了新的 commit ,故這裡 HEAD 代表
change這個 commit波浪線
~代表「前面的」數字和波浪線的配合,例如 :
~1代表往前一個 ;~3代表往前三個HEAD^:代表上一個 commit,等價於HEAD~1HEAD^^:代表上上個提交 commit,等價於HEAD~2HEAD和@是等價的,故上面也可以換成@^、@^^、@~2等等
所以在本範例 HEAD~1 會表示 init 這個 commit ,為 change 這個 commit 的前一個 commit。故運行完上述命令之後 :
- workspace 的文件不會變,還是修改過後的樣子
- staging 的紀錄也沒了,這就是
--mixed的作用,如果要推上去還要重新再git add才可以 - 可以發現
change這個在 git local 的 commit 沒了
git reset --soft HEAD~1soft 模式也會保留 workspace 上的修改,只是不會把 staging 的紀錄也給拿掉
git reset --hard HEAD~1如果就是想測銷掉所有修改包括 local, staging, workspace 可以使用 hard,但請謹慎
3. 整理自己的 commit
通常會是遇到下面狀況後,會選用 git rebase 整理 commit,讓別人看 code 可以輕鬆一些:
- 改寫 commit 的內容或訊息(edit / reword)
- 刪除錯誤的 commit(drop)
- 合併多個 commit 成一個(squash)
可以配合 -i 進入 interactive 模式,再來是選擇要更改的 commit 範圍,可以搭配 ~ 來表示範圍,例如說最近的 8 次 commits:
git rebase -i HEAD~8
接下來會進入 vim 編輯畫面,例如 :
pick 2a72a71 Add file1
pick 747f10f Add file2
pick 3b1ca36 Add file3
pick bb4c16c Add file4
pick 3ebea07 Add file5
按照從上到下順序,把這些 commit 串起來的。比較簡單的例如
變更 commit 的順序,如附例子可以把
pick 3b1ca36 Add file3移到最下面,儲存退出後順序就變成是1 -> 2 -> 4 -> 5 -> 3了刪除 commit 的話,就把前面的 「
pick指令改成drop」修改 commit message 的話,把 「
pick改成reword」,儲存離開之後,git 會執行套用,當執行到改成 reword commit 時會自動打開 vim 讓你改 messagegit rebase -i編輯的這個畫面中,是這筆 commit 的原本 message,這段是給人類看的,例如把Add file1改成Add file new,進行 rebase,不會套用改的訊息合併 commit 的話,譬如說想把
Add file3跟Add file4合併,那就把 Add file4 的「pick改成squash」,會把上一個 commit 合併一起





