# git 介紹

Git 是一個分散式版本控制系統,用來追蹤檔案的變更歷史。
每次修改都會被記錄,可以隨時回到過去的任意版本,也方便多人協作開發。

# git 安裝

這個部分暫時先跳過...

# 指令介紹

Git 的工作流程分為四個區域,指令負責在區域之間移動資料:

git addgit commitgit push工作目錄Working Directory暫存區Staging Area本地倉庫Local Repository雲端倉庫Remote Repository

# 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

顯示以下資訊

  1. 在哪個 branch
  2. 同步狀態
  3. 暫存區狀態
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>
更新於 閱讀次數 次