# git 介紹
Git 是一個分散式版本控制系統,用來追蹤檔案的變更歷史。
每次修改都會被記錄,可以隨時回到過去的任意版本,也方便多人協作開發。
# git 安裝
這個部分暫時先跳過...
# 指令介紹
Git 的工作流程分為四個區域,指令負責在區域之間移動資料:
# git add
把檔案加入暫存區
git add 檔案名稱 # 把整個檔案的變更加入暫存區 | |
git add . # 把當前目錄下所有變更加入 | |
git add -p # 互動式地選擇「要暫存哪一段」 |
# git commit
把暫存區的資料永久記錄進本地倉庫,並產生一個屬於這次提交的 hash 值
git commit -m "訊息" # 最常用,附上簡短說明 | |
git commit -am "訊息" # add + commit 合併(只對已追蹤的檔案有效) | |
git commit --amend # 修改最後一次 commit(訊息或內容) |
# git rm
在 git 上記述我刪掉的檔案,把刪除這個動作紀錄到暫存區
git rm 檔案名稱 # 刪除檔案,並把「刪除」這個動作加入暫存區 | |
git rm --cached 檔案名稱 # 只從追蹤清單移除,但保留實體檔案 |
# git revert
a -> b -> c
git revert b 會產生一個反向移除 b 的操作,執行完後多一個 d:
a -> b -> c -> d
不管 c 有沒有依賴 b,git revert 都會執行
只是有依賴的話會產生衝突,需要手動解決後
1. c 有依賴 b 的內容
a: 建立 login.js
b: 在 login.js 加入驗證邏輯 ← 你要 revert 這個
c: 在 login.js 的驗證邏輯上加入錯誤處理 ← 依賴 b
2. 如果 c 完全不依賴 b
a: 建立 login.js
b: 新增 README.md ← 你要 revert 這個(獨立的)
c: 修改 login.js 的樣式 ← 完全不相關
git revert HEAD # 撤銷最後一次 commit | |
git revert a3f9c2d # 撤銷指定的 commit |
# git reset
把 HEAD(目前所在的位置)移回某個指定的 commit,有三種模式,差別在於暫存區與工作目錄的資料保不保留:
| 模式 | 暫存區 | 工作目錄 |
|---|---|---|
--soft |
保留 | 保留 |
--mixed (預設) |
清空 | 保留 |
--hard |
清空 | 清空 |
git reset --hard bd9e027 會直接把 HEAD 移到 bd9e027 ,暫存區與工作目錄的所有變更都會強制丟棄,回到那個 commit 當時的乾淨狀態。
執行前:a → b → c → d(HEAD)
git reset --hard b
執行後:a → b(HEAD)
c、d 的變更全部消失
git reset --hard HEAD~1 # 回退一個 commit,丟棄所有變更 | |
git reset --hard bd9e027 # 回退到指定 commit,丟棄所有變更 | |
git reset --mixed HEAD~1 # 回退一個 commit,保留工作目錄的變更(預設) | |
git reset --soft HEAD~1 # 回退一個 commit,變更保留在暫存區 |
注意:
--hard會永久丟失未提交的變更,且已被 reset 掉的 commit 在git log中不再顯示。
若需要救回,可用git reflog找到舊的 hash 再 reset 回去。
# git status
顯示以下資訊
- 在哪個 branch
- 同步狀態
- 暫存區狀態
git status # 完整顯示 | |
git status -s # 簡短模式,每個檔案用兩個字母代表狀態 |
example
PS C:\Rabbir\Code\Rabbir-homework> git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: "\347\254\254\344\270\200\345\221\250/homework.md"
no changes added to commit (use "git add" and/or "git commit -a")
PS C:\Rabbir\Code\Rabbir-homework> git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: "\347\254\254\344\270\200\345\221\250/homework.md"
# git log
顯示完整的提交歷史
git log # 完整資訊:作者、時間、訊息 | |
git log --oneline # 每個 commit 壓縮成一行 | |
git log --oneline --graph # 加上分支的樹狀圖,視覺化非常有幫助 | |
git log --oneline --graph --all # 顯示所有分支的歷史(最常用的組合) | |
git log -p # 同時顯示每次 commit 的實際內容差異 | |
git log --author="名字" # 篩選特定作者的 commit |
# git diff
顯示修改的內容
git diff # 工作區 vs 暫存區(還沒 add 的變更) | |
git diff --staged # 暫存區 vs 最新 commit(已 add 但還沒 commit 的變更) | |
git diff HEAD # 工作區 vs 最新 commit(包含所有未提交的變更) | |
git diff 分支A 分支B # 比較兩個分支之間的差異 | |
git diff a3f9c2d b1e4d3c # 比較兩個特定 commit 的差異 |
# git branch
當要進行某些測試的修改,可以創建一個分支然後修改,不影響原本程式。
隨時可以切回原本的分支,可以決定要採用哪個版本哪個分支做切換。
git branch # 列出所有本地分支(* 表示目前所在) | |
git branch 分支名稱 # 建立新分支 | |
git branch -d 分支名稱 # 刪除已合併的分支 | |
git branch -D 分支名稱 # 強制刪除(不管有沒有合併) | |
git checkout -b 分支名稱 # 建立並立刻切換(最常用) | |
git switch -c 分支名稱 # 同上,較新的語法 |
# git merge
把兩個分支合併,前提是沒有衝突,有衝突會比較複雜
# 假設在 feature 分支開發完了,要合併回 main | |
git checkout main | |
git merge feature # 合併 feature 分支進來 | |
git merge --no-ff feature # 強制產生 merge commit(即使可以 fast-forward) |
# git rebase
更新指向的父節點
從 C 建立 feature,開始開發 D、E
同時隊友推了 F、G 到 main
main: A → B → C → F → G ← main 跑走了
\
feature: D → E ← 你還在舊的基礎上
這時執行 git rebase main ,就是把 D、E 搬到 G 後面:
main: A → B → C → F → G
\
feature: D' → E'
git checkout feature | |
git rebase main # 把 feature 的 commit 搬到 main 的最新點之後 |
# 簡介 rebase-interactive 模式
在 push 之前,對本地的 commit 歷史進行任意整理
example
r 52d28d2 新增第一周作業
# Rebase 95e9926..52d28d2 onto 95e9926 (1 command)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# create a merge commit using the original merge commit's
# message (or the oneline, if no original merge commit was
# specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
# to this position in the new commits. The <ref> is
# updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
git rebase -i HEAD~3 # 對最近 3 個 commit 進行互動式整理 | |
git rebase -i a3f9c2d # 對某個 commit 之後的所有 commit 整理 |
# 進階主題
# rebase 衝突與中止
當 rebase 撞到衝突時,git 會停在出問題的那個 commit,工作目錄裡會有 conflict marker。此時有三條路:
# 路 1:手動解衝突 | |
# 編輯衝突檔案 → git add → 繼續 | |
git add 衝突檔案 | |
git rebase --continue | |
# 路 2:跳過這個 commit(不要它了) | |
git rebase --skip | |
# 路 3:完全放棄這次 rebase,回到 rebase 前的狀態 | |
git rebase --abort |
--abort 是 rebase 出問題時最安全的退路 — 它會把分支指針還原到啟動 rebase 之前。重要:rebase 過程中 .git/ 會留下 rebase-merge/ 或 rebase-apply/ 資料夾,如果未完成的 rebase 沒清乾淨,下次操作可能撞到「You are currently rebasing」的提示。
# 判斷 commit 是否已在主線
rebase 撞衝突時,常見原因是「要 apply 的 commit 其實已經透過別的路徑進入主線了」。可以用:
# 檢查某個 commit 是不是 HEAD 的祖先 | |
git merge-base --is-ancestor <commit> HEAD && echo "已在歷史中" || echo "未在歷史中" | |
# 查某個 commit 在哪些 branch 上 | |
git branch --contains <commit> | |
# 用 commit message 跨分支搜尋 | |
git log --all --oneline --grep="關鍵字" |
如果確認 commit 已在主線下游,那就 git rebase --skip 跳過,不需要解衝突。
# git reflog — 救命工具
reflog 記錄 HEAD 每一次的移動(commit、checkout、reset、merge、rebase 都會留紀錄),即使 commit 已從 git log 消失,reflog 通常還能找回來。
git reflog # 看 HEAD 最近 30 筆移動 | |
git reflog -50 # 看更多 | |
git reflog show 分支名稱 # 看特定分支的歷史 |
輸出範例:
7aa86c91 HEAD@{0}: rebase (abort): returning to refs/heads/main
94c3d13b HEAD@{2}: commit: docs(claude): 新增任務優先順序規範
d2d67b91 HEAD@{3}: checkout: moving from test to main
每一行格式: <hash> HEAD@{N}: <操作類型>: <說明>
典型救援場景:
# 不小心 reset --hard 丟掉了 commit | |
git reflog # 找到丟掉前的 hash | |
git reset --hard <剛找到的 hash> # 救回來 | |
# rebase 失敗想完全還原(已經 abort 了還想退到 rebase 前的狀態) | |
git reflog # 找到 rebase 開始前的 HEAD | |
git reset --hard <那個 hash> |
reflog 預設保留 90 天,是 git 最強的後悔藥。
# 本地與遠端歧異 (diverged)
當 git status 顯示:
Your branch and 'origin/main' have diverged,
and have 1 and 439 different commits each, respectively.
意思是「本地有遠端沒有的 N 個 commit,遠端有本地沒有的 M 個 commit」,兩邊已分岔。
# 先搞清楚到底差什麼
git fetch origin # 先同步遠端資訊 | |
git log --oneline origin/main..main # 「本地有、遠端沒有」的 commit | |
git log --oneline main..origin/main # 「遠端有、本地沒有」的 commit | |
git log --oneline --graph --all -20 # 視覺化看分岔點 |
# 處理選項
| 情境 | 指令 |
|---|---|
| 本地 commit 沒價值(誤操作 / 殘留),想完全跟遠端對齊 | git reset --hard origin/main |
| 本地 commit 有價值,想接到遠端最新 | git rebase origin/main |
| 想保留兩邊歷史並合併 | git merge origin/main (會產生 merge commit) |
git reset --hard origin/main 前要先確認本地獨有的 commit 是否能丟 — 用 git log origin/main..main 看清楚。
# merge commit 與線圖品質
每次「先 push 失敗 → 再 pull → 再 push」這個流程,git 預設會產生一個 merge commit:
* a521b85e Merge branch 'main' of github.com/...
|\
| * 94c3d13b docs(claude): 新增任務優先順序規範
* | 7aa86c91 Merge branch 'production'
|\ \
如果只是同步遠端、本地實際上沒有真正的並行開發,這種 merge commit 是純結構性雜訊,會讓線圖變難讀。
# 預防:拉取時偏好 rebase 而非 merge
git pull --rebase # 單次:把本地 commit 改接到遠端最新之上,不產生 merge commit | |
git config pull.rebase true # 永久:所有 pull 都 rebase |
# 補救:移除已產生的 merge commit
如果 merge commit 還沒 push:
git reset --hard <merge 前一個 commit> |
如果已經 push 了(像本次案例),就需要 force push(見下節)。
# 工作流程上的警訊
「每個 feature commit 後面都跟一個 Merge branch 'main' into <branch> 」這種 pattern 通常代表流程有問題。常見原因:
- main 跟 test 兩個分支來回切換、每次切換都 merge 一次
- 多人 push 衝突太頻繁、每次都用 merge 解
解法:把長期需要同步的分支改用 git merge --ff-only (只允許 fast-forward,否則報錯),或乾脆減少分支數量。
# force push 的安全變體
force push 會改寫遠端已發布的歷史,預設危險。git 提供一個安全變體:
git push --force # 危險:強制覆蓋遠端,不管別人有沒有新 push | |
git push --force-with-lease # 安全:只在「遠端跟我上次看到的一樣」時才覆蓋 |
--force-with-lease 的保護機制:
情境:你準備 force push,但同事剛好也 push 了新東西
--force → 直接覆蓋,把同事的 commit 蓋掉(資料損失)
--force-with-lease → 偵測到遠端已變動 → 拒絕 push,提示你先 fetch
預設情況下永遠用 --force-with-lease 取代 --force 。
# 什麼時候真的需要 force push
- 移除已 push 的敏感資料(誤傳 token、密碼)
- 移除純結構性的 merge commit(線圖整理)
- rebase 已 push 的 commit(修 message、squash 等)
# 什麼時候絕對不要 force push
- 共用分支(main、master、develop、production)— 除非確定只有自己用
- 別人正在基於那個 branch 開發 — 會搞亂他們的歷史
# 流程建議
# Step 1: 確認要 reset 到的目標 commit | |
git log --oneline -5 | |
# Step 2: 用 diff 確認 reset 不會丟內容(如果只是丟結構性 merge) | |
git diff <目標> <目前> --stat # 預期應該是空的或可接受 | |
# Step 3: reset | |
git reset --hard <目標> | |
# Step 4: 安全 push | |
git push --force-with-lease origin <branch> |