☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
Git 中級

Git 進階技巧

Interactive Rebase(互動式變基)

💡 比喻:出版前的校稿 你寫了好幾章草稿(多個 commit), 在出版前(push 前)想要重新整理: 合併幾章、修改標題、調整順序。 Interactive Rebase 就是讓你在「出版前」做最後的編輯。

常用操作

# 互動式修改最近 3 個 commit
git rebase -i HEAD~3  # 開啟編輯器,讓你修改最近 3 個 commit

# 編輯器會顯示類似這樣的內容:
# pick abc1234 新增登入頁面
# pick def5678 修復 typo
# pick ghi9012 新增登入驗證

# 可用的指令:
# pick   = 保留這個 commit(不做任何修改)
# reword = 保留但修改 commit 訊息
# squash = 與前一個 commit 合併(保留訊息)
# fixup  = 與前一個 commit 合併(丟棄訊息)
# drop   = 刪除這個 commit
# edit   = 暫停在這個 commit,讓你修改內容

Squash — 合併多個 commit

# 場景:把零碎的 commit 合併成有意義的一個
git rebase -i HEAD~4  # 修改最近 4 個 commit

# 編輯器中修改:
# pick abc1234 新增登入頁面 HTML
# squash def5678 新增登入頁面 CSS
# squash ghi9012 新增登入頁面 JavaScript
# squash jkl3456 修復登入頁面 typo

# 儲存後會跳出另一個編輯器讓你寫合併後的 commit 訊息
# 最終結果:4 個 commit 變成 1 個

# 快速 squash(合併到前一個 commit,不修改訊息)
git commit --fixup abc1234  # 建立一個 fixup commit,標記要合併到 abc1234
git rebase -i --autosquash HEAD~5  # 自動排序 fixup commit 到對應位置

Reword — 修改 commit 訊息

# 場景:修改之前的 commit 訊息(不只是最新的)
git rebase -i HEAD~3  # 修改最近 3 個 commit

# 編輯器中修改:
# pick abc1234 新增登入頁面
# reword def5678 修固 typo    ← 把 pick 改成 reword
# pick ghi9012 新增驗證

# 儲存後會跳出編輯器讓你修改該 commit 的訊息
# 修改為:"修復登入頁面的 typo"

git bisect — 二分搜尋找 Bug

💡 比喻:翻書找錯字 你知道第 1 頁沒有錯字,第 100 頁有錯字, 不用一頁一頁翻,直接翻到第 50 頁: 有錯字?那問題在 1-50 頁之間,再翻第 25 頁... 這就是二分搜尋法,Git bisect 用同樣的方式找出引入 Bug 的 commit。

使用 bisect 找 Bug

# 步驟 1:開始 bisect
git bisect start  # 開始二分搜尋

# 步驟 2:標記目前版本是壞的
git bisect bad  # 告訴 Git:目前的版本有 Bug

# 步驟 3:標記一個已知沒問題的舊版本
git bisect good v1.0  # 告訴 Git:v1.0 版本是好的、沒有 Bug

# Git 會自動 checkout 到中間的 commit
# 你測試一下,然後告訴 Git 結果:

# 步驟 4:測試並回報
git bisect good  # 這個版本沒問題(Git 會再跳到下一個中間點)
git bisect bad   # 這個版本有問題(Git 會縮小搜尋範圍)

# 重複步驟 4 直到找到罪魁禍首
# Git 會顯示:abc1234 is the first bad commit

# 步驟 5:結束 bisect
git bisect reset  # 結束二分搜尋,回到原本的分支

自動化 bisect

# 如果有自動化測試,可以讓 bisect 自動跑
git bisect start  # 開始二分搜尋
git bisect bad HEAD  # 目前版本有 Bug
git bisect good v1.0  # v1.0 沒問題

# 自動執行測試腳本(回傳 0 = good,非 0 = bad)
git bisect run dotnet test  # 自動用測試結果來判斷好壞

# Git 會自動找出引入 Bug 的 commit!
git bisect reset  # 結束後回到原本的分支

git reflog — 救回誤刪的東西

💡 比喻:資源回收筒中的回收筒 即使你 reset --hard 或不小心刪了分支, Git 的 reflog 會記錄你最近 90 天內所有的操作歷史, 像是一個「超級資源回收筒」,幾乎什麼都能救回來。

查看 reflog

# 查看所有操作歷史
git reflog  # 顯示 HEAD 的所有移動紀錄

# 輸出範例:
# abc1234 HEAD@{0}: commit: 新增功能
# def5678 HEAD@{1}: checkout: moving from main to feature
# ghi9012 HEAD@{2}: commit: 修復 Bug
# jkl3456 HEAD@{3}: reset: moving to HEAD~2  ← 這裡做了 reset!

救回誤刪的 commit

# 場景:不小心做了 git reset --hard,丟掉了重要的 commit
git reflog  # 查看歷史,找到被丟掉的 commit hash

# 方法 1:直接 checkout 到那個 commit
git checkout abc1234  # 切換到被丟掉的 commit(detached HEAD 狀態)
git switch -c recovered-branch  # 建立新分支來保存它

# 方法 2:用 reset 回到那個狀態
git reset --hard abc1234  # 把目前分支強制指向那個 commit

# 方法 3:用 cherry-pick 撿回特定的 commit
git cherry-pick abc1234  # 把特定的 commit 套用到目前分支

救回刪掉的分支

# 場景:不小心刪了一個還沒合併的分支
git branch -D feature/important  # 糟糕!刪掉了重要分支

# 步驟 1:用 reflog 找到分支最後的 commit
git reflog  # 找到 feature/important 最後一個 commit 的 hash

# 步驟 2:從那個 commit 建立新分支
git branch feature/important abc1234  # 用找到的 hash 重新建立分支

# 分支就救回來了!
git switch feature/important  # 切換到救回的分支確認內容

Submodule vs Subtree

Submodule(子模組)

💡 比喻:引用外部函式庫 Submodule 像是在你的專案中放一個「書籤」指向另一個儲存庫, 專案不包含那個儲存庫的檔案本體,只記錄指向哪個版本。

# 新增子模組
git submodule add https://github.com/lib/shared-utils.git libs/shared-utils  # 新增子模組到 libs 資料夾

# 複製含有子模組的專案
git clone --recurse-submodules https://github.com/user/project.git  # 連同子模組一起複製

# 如果已經 clone 但忘記加 --recurse-submodules
git submodule init  # 初始化子模組設定
git submodule update  # 下載子模組的內容

# 更新子模組到最新版本
git submodule update --remote  # 把所有子模組更新到遠端最新版本

# 查看子模組狀態
git submodule status  # 顯示每個子模組目前指向的 commit

Subtree(子樹)

💡 比喻:把外部程式碼直接複製進來 Subtree 是直接把另一個儲存庫的檔案合併到你的專案中, 不需要額外的設定步驟,使用起來比 submodule 簡單。

# 新增子樹
git subtree add --prefix=libs/shared-utils https://github.com/lib/shared-utils.git main --squash  # 把外部 repo 的 main 分支加入到 libs 資料夾

# 更新子樹
git subtree pull --prefix=libs/shared-utils https://github.com/lib/shared-utils.git main --squash  # 拉取外部 repo 的最新變更

# 推送子樹的變更回去
git subtree push --prefix=libs/shared-utils https://github.com/lib/shared-utils.git main  # 把對子樹的修改推送回原始 repo

Submodule vs Subtree 比較

┌───────────────┬────────────────────────┬────────────────────────┐
│               │ Submodule              │ Subtree                │
├───────────────┼────────────────────────┼────────────────────────┤
│ 概念          │ 指向另一個 repo 的指標 │ 直接合併程式碼進來     │
│ 設定複雜度    │ 較複雜                 │ 較簡單                 │
│ Clone         │ 需要 --recurse         │ 正常 clone 即可        │
│ 更新          │ submodule update       │ subtree pull           │
│ 適用場景      │ 大型專案、嚴格版本控制 │ 小型共用程式庫         │
│ 新成員上手    │ 容易忘記初始化         │ 零額外步驟             │
└───────────────┴────────────────────────┴────────────────────────┘

Git Hooks(鉤子)

💡 比喻:保全系統 Git Hooks 就像門口的保全系統, 在你「進門」(commit)或「出門」(push)之前, 自動檢查你有沒有帶齊東西、有沒有可疑物品。

常用 Hooks

# Hook 檔案位於 .git/hooks/ 資料夾
ls .git/hooks/  # 查看可用的 hook 範本檔

# 常用的 Hook:
# pre-commit   → commit 之前執行(適合做程式碼檢查)
# commit-msg   → 檢查 commit 訊息格式
# pre-push     → push 之前執行(適合跑測試)
# post-merge   → merge 之後執行(適合自動安裝套件)

pre-commit Hook 範例

#!/bin/sh
# .git/hooks/pre-commit
# 在 commit 之前自動檢查程式碼品質

echo "🔍 正在檢查程式碼..."  # 顯示檢查開始訊息

# 執行 .NET 格式檢查
dotnet format --verify-no-changes  # 檢查程式碼是否符合格式規範
if [ $? -ne 0 ]; then  # 如果格式檢查失敗
    echo "❌ 程式碼格式不符,請先執行 dotnet format"  # 顯示錯誤訊息
    exit 1  # 回傳非零值,中止 commit
fi

# 執行單元測試
dotnet test --no-build  # 執行測試但不重新編譯
if [ $? -ne 0 ]; then  # 如果測試失敗
    echo "❌ 測試未通過,請修復後再 commit"  # 顯示錯誤訊息
    exit 1  # 回傳非零值,中止 commit
fi

echo "✅ 所有檢查通過!"  # 顯示檢查通過訊息
exit 0  # 回傳零值,允許 commit

使用 Husky(跨平台 Hook 管理工具)

# 安裝 Husky(Node.js 專案)
npm install husky --save-dev  # 安裝 Husky 到開發依賴

# 初始化 Husky
npx husky init  # 建立 .husky 資料夾和基本設定

# 新增 pre-commit hook
echo "dotnet format --verify-no-changes" > .husky/pre-commit  # 設定 commit 前自動檢查格式

# .NET 專案可以用 dotnet-format 的 local tool
dotnet tool install dotnet-format  # 安裝 dotnet-format 工具

commit-msg Hook(檢查 commit 訊息格式)

#!/bin/sh
# .git/hooks/commit-msg
# 檢查 commit 訊息是否符合 Conventional Commits 格式

commit_msg=$(cat "$1")  # 讀取 commit 訊息內容

# 檢查格式:type(scope): description
pattern="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,72}$"  # 定義允許的格式模式

if ! echo "$commit_msg" | grep -qE "$pattern"; then  # 如果訊息不符合格式
    echo "❌ commit 訊息格式不正確!"  # 顯示錯誤訊息
    echo "正確格式:type(scope): description"  # 顯示正確格式
    echo "例如:feat(login): 新增密碼重設功能"  # 顯示範例
    echo "允許的 type:feat, fix, docs, style, refactor, test, chore"  # 列出允許的類型
    exit 1  # 中止 commit
fi

exit 0  # 格式正確,允許 commit

大檔案處理:Git LFS

💡 比喻:大行李用託運 搭飛機時,小包包可以手提帶上機(一般 Git), 但大行李箱要託運(Git LFS)。 Git LFS 把大檔案「託運」到專門的伺服器, Git 只記錄一個「託運單號」(指標),讓 repo 保持輕量。

安裝與設定 Git LFS

# 安裝 Git LFS
# Windows:Git for Windows 通常已內建
# macOS
brew install git-lfs  # 透過 Homebrew 安裝 Git LFS

# Linux
sudo apt install git-lfs  # 透過 apt 安裝 Git LFS

# 初始化 Git LFS(每台電腦做一次)
git lfs install  # 在系統層級啟用 Git LFS

# 追蹤特定類型的大檔案
git lfs track "*.psd"  # 追蹤所有 Photoshop 檔案
git lfs track "*.zip"  # 追蹤所有 ZIP 壓縮檔
git lfs track "*.mp4"  # 追蹤所有影片檔案
git lfs track "*.dll"  # 追蹤所有 DLL 檔案

# 確認 .gitattributes 已被追蹤
git add .gitattributes  # 把 LFS 的追蹤設定加入版本控制

# 查看目前 LFS 追蹤的檔案類型
git lfs track  # 列出所有被 LFS 追蹤的模式

# 查看 LFS 管理的檔案
git lfs ls-files  # 列出所有被 LFS 管理的檔案

LFS 日常使用

# 正常使用 git add / commit / push,LFS 會自動處理
git add design.psd  # 加入大檔案(LFS 自動接管)
git commit -m "新增設計稿"  # 正常提交
git push origin main  # 推送時 LFS 會自動上傳大檔案到 LFS 伺服器

# 複製含有 LFS 的專案
git clone https://github.com/user/project.git  # 正常 clone,LFS 自動下載大檔案

# 如果只想下載指標(不下載大檔案內容)
GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/user/project.git  # 跳過 LFS 檔案下載

# 之後需要時再手動下載
git lfs pull  # 手動下載所有 LFS 管理的檔案

.gitattributes 設定

💡 比喻:入境申報表 .gitattributes 就像海關的申報表, 告訴 Git 每種檔案要怎麼處理: 文字檔要自動轉換換行符號、二進位檔不要嘗試做 diff。

常用 .gitattributes 設定

# .gitattributes 檔案內容

# === 文字檔案處理 ===
# 自動偵測文字檔並統一換行符號
* text=auto  # 讓 Git 自動判斷檔案類型並處理換行符號

# 明確指定文字檔案(使用 LF 換行)
*.cs text eol=lf  # C# 檔案統一用 LF 換行
*.csproj text eol=lf  # 專案檔統一用 LF 換行
*.sln text eol=lf  # 解決方案檔統一用 LF 換行
*.json text eol=lf  # JSON 檔案統一用 LF 換行
*.md text eol=lf  # Markdown 檔案統一用 LF 換行
*.yml text eol=lf  # YAML 檔案統一用 LF 換行
*.yaml text eol=lf  # YAML 檔案統一用 LF 換行

# Windows 批次檔需要用 CRLF
*.bat text eol=crlf  # 批次檔統一用 CRLF 換行
*.cmd text eol=crlf  # 命令檔統一用 CRLF 換行

# === 二進位檔案(不做 diff)===
*.png binary  # PNG 圖片標記為二進位
*.jpg binary  # JPG 圖片標記為二進位
*.gif binary  # GIF 圖片標記為二進位
*.ico binary  # 圖示檔標記為二進位
*.pdf binary  # PDF 文件標記為二進位
*.zip binary  # ZIP 壓縮檔標記為二進位

# === Git LFS(大檔案)===
*.psd filter=lfs diff=lfs merge=lfs -text  # Photoshop 檔案用 LFS 管理
*.ai filter=lfs diff=lfs merge=lfs -text  # Illustrator 檔案用 LFS 管理
*.mp4 filter=lfs diff=lfs merge=lfs -text  # 影片檔案用 LFS 管理

# === 語言統計(GitHub 語言識別)===
docs/api/** linguist-generated=true  # 自動產生的 API 文件不計入語言統計
*.min.js linguist-vendored=true  # 壓縮的 JS 標記為第三方檔案

.NET 專案建議的 .gitattributes

# .NET 專案的 .gitattributes
# 自動偵測文字檔
* text=auto  # Git 自動判斷並處理換行符號

# .NET 原始碼
*.cs text diff=csharp  # C# 檔案用 csharp diff 演算法
*.cshtml text diff=html  # Razor 檔案用 html diff 演算法
*.razor text diff=html  # Blazor 元件用 html diff 演算法
*.xaml text  # XAML 檔案標記為文字
*.csproj text  # 專案檔標記為文字
*.sln text  # 解決方案檔標記為文字

# 設定檔
*.json text  # JSON 標記為文字
*.xml text  # XML 標記為文字
*.config text  # 設定檔標記為文字

# 指令碼
*.sh text eol=lf  # Shell 腳本強制 LF 換行
*.ps1 text eol=crlf  # PowerShell 強制 CRLF 換行
*.bat text eol=crlf  # 批次檔強制 CRLF 換行

# 二進位
*.dll binary  # DLL 標記為二進位
*.exe binary  # EXE 標記為二進位
*.nupkg binary  # NuGet 套件標記為二進位

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:對已推送的 commit 做 interactive rebase

# 錯誤:已經 push 到遠端的 commit,用 rebase 改寫歷史
git push origin feature/login  # 已經推送了
git rebase -i HEAD~3  # 改寫了已推送的 commit 歷史
git push --force origin feature/login  # 強制推送覆蓋遠端

# ✅ 正確:只對「還沒 push」的 commit 做 rebase
# 或者使用 --force-with-lease(較安全的強制推送)
git push --force-with-lease origin feature/login  # 只有在遠端沒有新 commit 時才會成功

為什麼錯? 改寫已推送的歷史,其他正在這個分支上工作的人會遇到歷史不一致的問題。如果必須強制推送,用 --force-with-lease--force 安全,因為它會檢查遠端是否有你不知道的新 commit。

❌ 錯誤 2:大檔案沒用 LFS 直接 commit

# 錯誤:直接把大檔案加入 Git
git add training-data.zip  # 500MB 的檔案直接加入 Git
git commit -m "新增訓練資料"  # 提交後 repo 暴增 500MB
git push origin main  # push 超慢,而且歷史中永遠佔 500MB

# ✅ 正確:先設定 LFS 再加入
git lfs track "*.zip"  # 先設定 LFS 追蹤 zip 檔案
git add .gitattributes  # 加入 LFS 設定
git add training-data.zip  # 這次 Git 會用 LFS 處理大檔案
git commit -m "新增訓練資料(LFS)"  # 提交,Git 只存指標

為什麼錯? Git 不適合存放大型二進位檔案。每個版本都會完整保存,即使後來刪除,歷史中仍然佔用空間。repo 會越來越大,clone 和 push 都會變慢。

❌ 錯誤 3:不知道 reflog,以為 reset --hard 就沒救了

# 錯誤:以為 reset --hard 後資料就永遠消失了
git reset --hard HEAD~3  # 丟掉最近 3 個 commit
# "完了完了,我的程式碼不見了..."

# ✅ 正確:用 reflog 救回來
git reflog  # 查看操作歷史
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: 重要的功能  ← 找到了!

git reset --hard def5678  # 回到 reset 之前的狀態,資料全部救回來

為什麼錯? 很多人以為 reset --hard 是不可逆的操作,其實 Git 的 reflog 會保留所有操作紀錄約 90 天。只要知道 reflog,幾乎所有操作都可以復原。


📝 本章重點整理

技巧 用途 適用情境
Interactive Rebase 整理 commit 歷史 Push 前整理零碎的 commit
git bisect 二分搜尋找 Bug 知道某版本有 Bug 但不確定是哪個 commit
git reflog 救回誤刪的資料 reset --hard 或刪錯分支後的救命工具
Submodule/Subtree 引用外部儲存庫 多專案共用程式碼
Git Hooks 自動化檢查 commit 前自動跑測試/格式檢查
Git LFS 大檔案管理 追蹤圖片、影片等大型二進位檔
.gitattributes 檔案處理規則 統一換行符號、標記二進位檔

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊