開場:你遇過這些問題嗎?
你是否曾經:
- 空目錄無法提交,不知道為什麼
logs/資料夾在 Git 中消失了? - CRLF vs LF 衝突,每次 pull 都看到一堆「看起來沒改但 diff 顯示整個檔案都變了」?
.gitignore無效,明明加了規則但檔案還是被追蹤?- CI/CD 失敗,在本地跑得好好的,上 GitHub Actions 就出錯?
- 不知道該不該提交
.github/、.gitkeep、.gitattributes?
這些問題的根源都來自於「不理解 Git 檔案系統的邊界」。
Key Insight: Git 隱藏檔案分為「官方支援」與「社群約定」兩類,混淆兩者是跨團隊協作問題的主要來源。
在幾乎所有的開發專案中,我們都能看到許多以 .git 開頭的檔案或資料夾。根據 Git 官方文檔,有些是 Git 官方的一部分(如 .gitignore、.gitattributes),有些是平台擴充(如 .github/、.gitlab-ci.yml),也有一些是社群慣例(如 .gitkeep)。
本篇文章將從實戰角度系統性整理 官方 vs 非官方 Git 檔案,並提供多人協作、DevOps 整合、跨平台開發的最佳實踐。
一、Git 官方檔案總覽(Git 原生支援)
這些檔案或目錄是 Git 原生設計的一部分,其語法、用途與效力均受 Git 官方支援。
| 檔案名稱 | 官方屬性 | 用途 | 常見範例 |
|---|---|---|---|
.git/ |
✅ 核心 | 儲存整個版本控制資料庫(物件、索引、分支、設定) | .git/config, .git/refs/, .git/objects/ |
.gitignore |
✅ 官方 | 定義哪些檔案或目錄不應被 Git 追蹤 | node_modules/, .env, *.log |
.gitattributes |
✅ 官方 | 設定文字檔正規化、合併策略、LFS 管理 | *.sh text eol=lf, *.zip binary |
.gitmodules |
✅ 官方 | 記錄子模組(Submodule)的來源與路徑 | [submodule "lib"] |
.gitconfig |
✅ 官方 | 儲存使用者層級或系統層級設定 | [user] name=... |
.mailmap |
✅ 官方 | 統一作者名稱與 email(歷史貢獻統計) | 合併不同 email 為同一作者 |
.git-blame-ignore-revs |
✅ 半官方 | 指定 git blame 忽略的 commit |
忽略格式化 commit |
二、Git 檔案層級架構全景圖
以下 Mermaid 圖表展示了 Git 檔案系統的完整分層結構:
graph TB
subgraph "Git 生態系統"
A[Git 核心層]
B[版本控制層]
C[平台擴充層]
D[社群慣例層]
E[開發者私有層]
end
A --> A1[.git/ 目錄]
A --> A2[.gitconfig]
B --> B1[.gitignore]
B --> B2[.gitattributes]
B --> B3[.gitmodules]
B --> B4[.mailmap]
C --> C1[.github/]
C --> C2[.gitlab-ci.yml]
C --> C3[.gitreview]
D --> D1[.gitkeep]
D --> D2[.placeholder]
E --> E1[.git/info/exclude]
E --> E2[.gitignore_global]
style A fill:#667eea,color:#fff
style B fill:#764ba2,color:#fff
style C fill:#f7f7fd,stroke:#667eea
style D fill:#e8e8f5,stroke:#764ba2
style E fill:#d4d4f0,stroke:#666
關鍵觀念:五層分離原則
| 層級 | 誰管理 | 是否提交 | 範例 |
|---|---|---|---|
| 核心層 | Git 自己 | ❌ 絕不 | .git/ |
| 版本控制層 | 團隊共享 | ✅ 必須 | .gitignore, .gitattributes |
| 平台擴充層 | 平台特定 | ✅ 依平台 | .github/, .gitlab-ci.yml |
| 社群慣例層 | 非官方約定 | ⚙️ 視情況 | .gitkeep |
| 開發者私有層 | 個人環境 | ❌ 不要 | .git/info/exclude |
三、實戰場景一:解決「.gitignore 為什麼無效?」
問題現場
$ echo "secret.key" >> .gitignore
$ git add .gitignore
$ git commit -m "Add gitignore"
# 但 secret.key 還是被追蹤!
$ git status
modified: secret.key
根本原因
Key Insight:
.gitignore只對「尚未被追蹤的檔案」生效。已被追蹤的檔案必須先用git rm --cached移除追蹤狀態。
根據 Git 官方 gitignore 文檔,如果檔案已經 git add 過,必須先移除追蹤:
# 停止追蹤但保留本地檔案
git rm --cached secret.key
# 確認已加入 .gitignore
echo "secret.key" >> .gitignore
# 提交變更
git add .gitignore
git commit -m "Stop tracking secret.key"
進階技巧:三層忽略策略
# 1. 團隊共享:.gitignore(提交到版本庫)
node_modules/
dist/
*.log
# 2. 專案私有:.git/info/exclude(不會被提交)
# 適合個人 IDE 設定
.vscode/
.idea/
.DS_Store
# 3. 全域設定:~/.gitignore_global(所有專案通用)
# 設定方式:
git config --global core.excludesfile ~/.gitignore_global
最佳實踐: – ✅ 團隊協作檔案(如 node_modules/)寫在 .gitignore – ✅ 個人環境檔案(如 .DS_Store)寫在 ~/.gitignore_global – ✅ 臨時測試檔案寫在 .git/info/exclude
四、實戰場景二:跨平台開發的 CRLF 地獄
問題現場
Windows 開發者提交程式碼後,Mac/Linux 開發者 pull 下來發現:
$ git diff
-#!/bin/bash^M
-echo "Hello"^M
+#!/bin/bash
+echo "Hello"
整個檔案顯示「全部改動」,但其實只是行尾符號不同(CRLF vs LF)。
解決方案:使用 .gitattributes 強制正規化
Best Practice: 每個專案都應該有
.gitattributes檔案,使用* text=auto自動正規化行尾符號。 – 原因:避免跨平台開發時的 CRLF/LF 衝突 – 例外:純 Windows 專案可不設定
根據 Git 官方 gitattributes 文檔,建議的設定如下:
# .gitattributes
# 自動偵測文字檔並正規化
* text=auto
# 強制特定檔案使用 LF(Unix 風格)
*.sh text eol=lf
*.py text eol=lf
*.md text eol=lf
# 強制特定檔案使用 CRLF(Windows 風格)
*.bat text eol=crlf
*.ps1 text eol=crlf
# 二進位檔案不處理
*.png binary
*.jpg binary
*.zip binary
為什麼需要這樣做?
| 情境 | 不使用 .gitattributes | 使用 .gitattributes |
|---|---|---|
| Windows 提交 | 可能夾帶 CRLF | Git 自動轉為 LF 儲存 |
| Mac 檢出 | 收到 CRLF,diff 爆炸 | 統一 LF,diff 乾淨 |
| Shell 腳本 | 可能無法執行(CRLF 錯誤) | 強制 LF,確保正常執行 |
最佳實踐: – ✅ 每個專案都應該有 .gitattributes – ✅ 提交後執行 git add --renormalize . 重新正規化 – ✅ 團隊統一設定 core.autocrlf
五、實戰場景三:「.gitkeep 是什麼?我該用嗎?」
問題現場
你想提交這個目錄結構:
project/
├── src/
│ └── main.py
└── logs/ # 空目錄,但 Git 不追蹤
提交後,logs/ 資料夾消失了!因為 Git 不追蹤空目錄。
社群解決方案:.gitkeep
# 建立空檔案讓 Git 追蹤目錄
touch logs/.gitkeep
git add logs/.gitkeep
git commit -m "Add logs directory"
重要觀念:.gitkeep 不是 Git 官方功能
Definition:
.gitkeep是一個社群慣例檔案,用於讓 Git 追蹤空目錄。它不是 Git 官方規範的一部分,任何檔名(如.placeholder、.empty)都能達到相同效果。
| 項目 | 說明 |
|---|---|
| 官方支援 | ❌ 不是 Git 規範的一部分 |
| 作用原理 | 只是個空檔案,讓目錄「非空」所以能被追蹤 |
| 替代方案 | 可以用 .placeholder、.empty、README.md 等任何檔名 |
| 是否提交 | ✅ 可以提交(如果目錄必須存在) |
實際案例:何時需要 .gitkeep?
# 案例 1:應用程式需要的空目錄
uploads/.gitkeep # 使用者上傳檔案目錄
cache/.gitkeep # 快取目錄
logs/.gitkeep # 日誌目錄
# 案例 2:建置過程需要的目錄
dist/.gitkeep # 但應該加入 .gitignore
build/output/.gitkeep # 編譯輸出目錄
# 案例 3:Docker volume 掛載點
docker/volumes/db/.gitkeep
最佳實踐: – ✅ 如果目錄是「運行時需要」,使用 .gitkeep – ❌ 如果目錄是「建置產物」,不要提交(加入 .gitignore) – ⚙️ 也可以在 README.md 或腳本中 mkdir -p 自動建立
六、實戰場景四:CI/CD 與 Git 檔案的整合
GitHub Actions 範例
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# 檢查 .gitattributes 是否有效
- name: Check line endings
run: |
git ls-files --eol | grep 'i/crlf' && exit 1 || exit 0
# 檢查是否有未追蹤的敏感檔案
- name: Check for secrets
run: |
if git ls-files | grep -E '\.(env|key|pem)$'; then
echo "❌ 發現敏感檔案被追蹤!"
exit 1
fi
GitLab CI 範例
# .gitlab-ci.yml
stages:
- validate
- test
validate_git_files:
stage: validate
script:
# 確認 .gitattributes 存在
- test -f .gitattributes || (echo "缺少 .gitattributes" && exit 1)
# 確認沒有 CRLF 問題
- git ls-files --eol | grep 'i/crlf' && exit 1 || exit 0
Docker 與 .gitignore 的協作
# Dockerfile
FROM node:18
WORKDIR /app
# 複製 package.json(不受 .gitignore 影響)
COPY package*.json ./
# 安裝依賴
RUN npm ci
# 複製原始碼(會遵循 .dockerignore)
COPY . .
# 建置
RUN npm run build
# .dockerignore(類似 .gitignore,但給 Docker 用)
node_modules/
.git/
.github/
*.log
.env
關鍵差異: | 項目 | .gitignore | .dockerignore | |——|———–|—————| | 作用對象 | Git 版本控制 | Docker 建置上下文 | | 何時使用 | 決定哪些檔案不追蹤 | 決定哪些檔案不傳給 Docker | | 常見內容 | node_modules/, .env | .git/, README.md, *.md |
七、.git/ 內部結構深入解析(進階)
雖然你不應該手動編輯 .git/ 目錄,但理解其結構能幫助你除錯:
.git/
├── HEAD # 指向當前分支
├── config # 專案層級設定
├── description # 給 GitWeb 用的描述
├── hooks/ # Git hooks 腳本
│ ├── pre-commit # 提交前執行
│ ├── post-merge # 合併後執行
│ └── pre-push # 推送前執行
├── info/
│ └── exclude # 私有忽略清單
├── objects/ # Git 物件資料庫(commit、tree、blob)
│ ├── pack/ # 壓縮後的物件
│ └── [0-9a-f]{2}/ # 物件依 hash 前兩碼分類
├── refs/
│ ├── heads/ # 本地分支
│ ├── remotes/ # 遠端分支
│ └── tags/ # 標籤
└── index # 暫存區(staging area)
實用除錯指令
# 檢視當前分支
cat .git/HEAD
# 輸出:ref: refs/heads/main
# 檢視分支指向的 commit
cat .git/refs/heads/main
# 輸出:a1b2c3d4...(commit hash)
# 檢視物件類型
git cat-file -t a1b2c3d4
# 輸出:commit / tree / blob
# 檢視物件內容
git cat-file -p a1b2c3d4
# 檢查資料庫完整性
git fsck --full
# 清理不必要的物件
git gc --aggressive
八、.gitattributes 進階應用:合併策略與 Git LFS
自訂合併策略
# .gitattributes
# package-lock.json 合併時使用 union 策略(保留雙方)
package-lock.json merge=union
# 資料庫遷移檔案永遠使用我們的版本
db/migrations/* merge=ours
# 設定檔永遠使用他們的版本
config/production.yml merge=theirs
Git LFS(Large File Storage)整合
當專案有大型二進位檔案(如影片、3D 模型、資料集)時:
# 安裝 Git LFS
git lfs install
# .gitattributes 設定
*.psd filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
# 檢視 LFS 追蹤的檔案
git lfs ls-files
# 檢視 LFS 儲存用量
git lfs env
為什麼需要 Git LFS? – ❌ 一般 Git:每次提交都儲存完整檔案,倉庫快速膨脹 – ✅ Git LFS:只儲存指標,實際檔案存在 LFS 伺服器
九、檔案提交決策流程圖
當你不確定某個檔案是否該提交時,使用這個決策樹:
flowchart TD
Start([發現 .git 開頭的檔案]) --> Q1{是否在 .git/ 目錄內?}
Q1 -->|是| Never[❌ 絕不提交
這是 Git 內部資料]
Q1 -->|否| Q2{是否為官方 Git 檔案?}
Q2 -->|是| Q3{是全域設定嗎?}
Q2 -->|否| Q4{是平台特定檔案?}
Q3 -->|是
.gitconfig| Never2[❌ 不提交
屬於個人設定]
Q3 -->|否
.gitignore
.gitattributes
.gitmodules| Commit[✅ 應該提交
團隊共享規則]
Q4 -->|是
.github/
.gitlab-ci.yml| CI[✅ 提交
若使用該平台]
Q4 -->|否| Q5{是社群慣例檔案?}
Q5 -->|是
.gitkeep| Keep[⚙️ 視情況
保留空目錄時提交]
Q5 -->|否| Unknown[⚠️ 不明檔案
先查證用途]
style Never fill:#ff6b6b,color:#fff
style Never2 fill:#ff6b6b,color:#fff
style Commit fill:#51cf66,color:#fff
style CI fill:#51cf66,color:#fff
style Keep fill:#ffd43b,color:#333
style Unknown fill:#ff922b,color:#fff
十、常見問題 FAQ
Q1: .gitignore 無法忽略已追蹤的檔案怎麼辦?
A: 使用 git rm --cached 移除追蹤狀態:
# 移除單一檔案追蹤
git rm --cached secret.key
# 移除整個目錄追蹤
git rm -r --cached logs/
# 套用新的 .gitignore 規則
git add .gitignore
git commit -m "Update gitignore and stop tracking files"
檢查是否生效:
git check-ignore -v secret.key
# 輸出:.gitignore:5:secret.key secret.key
Q2: 為什麼我的 Shell 腳本在 CI/CD 上無法執行?
A: 通常是 CRLF 問題。檢查並修復:
# 檢查檔案的行尾符號
git ls-files --eol | grep script.sh
# 修復方式 1:使用 .gitattributes 強制 LF
echo "*.sh text eol=lf" >> .gitattributes
git add --renormalize .
git commit -m "Fix line endings for shell scripts"
# 修復方式 2:本地轉換(臨時)
dos2unix script.sh # Linux/Mac
Q3: .github/ 和 .git/ 有什麼區別?
A: 完全不同的兩個東西:
| 項目 | .git/ | .github/ |
|---|---|---|
| 屬性 | Git 官方核心目錄 | GitHub 平台擴充目錄 |
| 作用 | 儲存版本控制資料庫 | 儲存 GitHub Actions、Issue 模板 |
| 是否提交 | ❌ 絕不提交 | ✅ 應該提交(若使用 GitHub) |
| 跨平台 | ✅ 所有 Git 環境通用 | ❌ 僅 GitHub 平台有效 |
| 範例 | .git/config, .git/objects/ |
.github/workflows/ci.yml |
Q4: 如何在多個平台(GitHub + GitLab)使用不同的 CI/CD?
A: 兩者可以共存:
project/
├── .github/
│ └── workflows/
│ └── ci.yml # GitHub Actions
├── .gitlab-ci.yml # GitLab CI
└── azure-pipelines.yml # Azure DevOps
注意事項: – ✅ 兩個平台會各自執行自己的 CI/CD – ⚠️ 需要分別維護兩套設定檔 – 建議使用相同的測試指令(如 npm test)保持一致
Q5: .gitattributes 的 merge 策略有哪些?
A: 主要有三種:
# 1. union:保留雙方變更
package-lock.json merge=union
# 2. ours:永遠使用我們的版本
db/schema.sql merge=ours
# 3. theirs:永遠使用他們的版本
config/production.yml merge=theirs
# 4. binary:視為二進位檔,衝突時必須手動解決
*.docx binary
實際應用:
# 自動產生的檔案使用 union
package-lock.json merge=union
yarn.lock merge=union
# 資料庫遷移檔案使用 ours(保護生產環境)
migrations/*.sql merge=ours
Q6: 如何讓 git blame 忽略格式化 commit?
A: 使用 .git-blame-ignore-revs 檔案:
# .git-blame-ignore-revs
# 格式化 commit(Prettier)
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
# 重構 commit(ESLint autofix)
b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6a1
# 使用方式
git blame --ignore-revs-file .git-blame-ignore-revs file.js
# 設為預設
git config blame.ignoreRevsFile .git-blame-ignore-revs
Q7: 如何檢查專案中所有的 Git 設定?
A: 分層檢查:
# 1. 系統層級設定
git config --system --list
# 2. 全域設定(使用者層級)
git config --global --list
# 3. 專案層級設定
git config --local --list
# 4. 檢視特定設定值
git config user.name
git config core.autocrlf
# 5. 檢視所有設定(包含來源)
git config --list --show-origin
Q8: 如何清理 Git 倉庫中的敏感資訊?
A: 使用 git filter-repo(推薦)或 BFG Repo-Cleaner:
# 安裝 git-filter-repo
pip3 install git-filter-repo
# 移除特定檔案的歷史
git filter-repo --path secret.key --invert-paths
# 移除特定文字(如 API Key)
git filter-repo --replace-text <(echo "SECRET_API_KEY==>REDACTED")
# 強制推送(警告:會改寫歷史)
git push origin --force --all
git push origin --force --tags
⚠️ 重要:此操作會改寫歷史,所有協作者必須重新 clone。
Q9: 為什麼 .gitmodules 必須提交?
A: 因為它記錄了子模組的設定,團隊成員才能正確初始化:
# .gitmodules 範例
[submodule "libs/common"]
path = libs/common
url = https://github.com/company/common-lib.git
branch = main
# 初始化子模組(需要 .gitmodules)
git submodule update --init --recursive
沒有 .gitmodules 會怎樣? – ❌ 其他人 clone 後看不到子模組目錄 – ❌ git submodule update 會失敗 – ❌ CI/CD 無法自動初始化子模組
Q10: 如何安全地檢視 .git/ 內部結構?
A: 使用唯讀指令:
# 檢視目錄結構(限制深度避免過多輸出)
tree .git/ -L 2
# 檢視 HEAD 指向
cat .git/HEAD
# 檢視最近的 commit 內容
git cat-file -p HEAD
# 檢視物件類型
git cat-file -t HEAD
# 檢查倉庫完整性
git fsck --full
# 檢視所有 refs
git show-ref
⚠️ 永遠不要手動編輯 .git/ 內的檔案,除非你完全理解後果。
十一、官方與非官方檔案完整對照表
| 類別 | 檔案 | 官方狀態 | 是否提交 | 用途 |
|---|---|---|---|---|
| 核心層 | .git/ |
✅ 官方 | ❌ 絕不 | Git 資料庫 |
| 核心層 | .gitconfig |
✅ 官方 | ❌ 個人設定 | 使用者設定 |
| 版本控制層 | .gitignore |
✅ 官方 | ✅ 必須 | 忽略檔案規則 |
| 版本控制層 | .gitattributes |
✅ 官方 | ✅ 必須 | 文字正規化、合併策略 |
| 版本控制層 | .gitmodules |
✅ 官方 | ✅ 必須 | 子模組設定 |
| 版本控制層 | .mailmap |
✅ 官方 | ✅ 可選 | 作者名稱統一 |
| 版本控制層 | .git-blame-ignore-revs |
✅ 半官方 | ✅ 可選 | 忽略格式化 commit |
| 平台擴充層 | .github/ |
❌ 平台 | ✅ 若用 GitHub | GitHub Actions、模板 |
| 平台擴充層 | .gitlab-ci.yml |
❌ 平台 | ✅ 若用 GitLab | GitLab CI/CD |
| 平台擴充層 | .gitreview |
❌ 平台 | ✅ 若用 Gerrit | Gerrit Code Review |
| 社群慣例層 | .gitkeep |
❌ 慣例 | ⚙️ 視情況 | 保留空目錄 |
| 私有層 | .git/info/exclude |
✅ 官方 | ❌ 不提交 | 私有忽略清單 |
| 私有層 | ~/.gitignore_global |
✅ 官方 | ❌ 不提交 | 全域忽略規則 |
十二、最佳實踐檢查清單
✅ 每個專案應該有的檔案
✅ .gitignore # 忽略 node_modules、.env、*.log
✅ .gitattributes # 統一行尾符號(* text=auto)
✅ README.md # 專案說明
⚙️ .gitkeep # 若有空目錄需求(如 logs/、uploads/)
⚙️ .git-blame-ignore-revs # 若有大規模格式化 commit
✅ 跨平台開發團隊必須有的設定
# .gitattributes
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
*.png binary
*.jpg binary
✅ CI/CD 專案應該有的設定
# .github/workflows/validate-git.yml
name: Validate Git Configuration
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check .gitattributes exists
run: test -f .gitattributes
- name: Check for CRLF issues
run: |
if git ls-files --eol | grep 'i/crlf'; then
echo "❌ 發現 CRLF 問題"
exit 1
fi
- name: Check for untracked secrets
run: |
if git ls-files | grep -E '\.(env|key|pem)$'; then
echo "❌ 發現敏感檔案被追蹤"
exit 1
fi
十三、結論與關鍵要點
理解 Git 檔案系統的邊界,能幫助你:
- ✅ 避免衝突:透過
.gitattributes統一行尾符號 - ✅ 保護隱私:正確使用
.gitignore與.git/info/exclude - ✅ 提升協作:團隊共享
.gitignore與.gitattributes - ✅ 優化 CI/CD:善用
.github/或.gitlab-ci.yml - ✅ 除錯更快:理解
.git/內部結構
快速參考表
| 需求 | 使用檔案 | 範例 |
|---|---|---|
| 忽略檔案 | .gitignore |
node_modules/, *.log |
| 統一行尾 | .gitattributes |
* text=auto, *.sh eol=lf |
| 保留空目錄 | .gitkeep |
logs/.gitkeep |
| 私有忽略 | .git/info/exclude |
.vscode/, .idea/ |
| CI/CD | .github/ 或 .gitlab-ci.yml |
GitHub Actions |
| 大檔案 | .gitattributes + Git LFS |
*.psd filter=lfs |
Sources
- Git Official Documentation – Git 官方文檔首頁
- gitignore – Specifies intentionally untracked files – .gitignore 官方規範
- gitattributes – Defining attributes per path – .gitattributes 官方規範
- Pro Git Book (繁體中文版) – 官方推薦的 Git 完整教學
- GitHub Actions Documentation – GitHub Actions 官方指南
- GitLab CI/CD Documentation – GitLab CI/CD 官方文件
- Git LFS Official Site – Git Large File Storage 官方網站