# 前言

今天的主題其實在之前開始介紹如何使用 Git 時就可以先說明,不過還是希望能先把基本觀念一次說明到位,讓讀者先對 Git 有一定的認識再說,畢竟設定類的說明,隨時補都來得及。

回到主題,實務上的專案中,總是會有一些「設定檔」、「編譯檔」、「日誌 (log)」,之類不影響專案本體的內容,他們並不需要 (有些甚至不可以) 被記錄到儲存庫中,該如何定義那些「不想被版控」的資料,會是個重要的問題。

為此 Git 提供了一個方式讓我們去定義這些內容:設定 .gitignore 檔案。

# 設定 .gitignore 檔案

首先來說文解字一下, ignore 中文是「忽略」的意思,所以 .gitignore 是用來定義讓 Git 忽略的資料的檔案。

想定義能讓 Git 忽略的資料也很簡單:

  1. 直接在工作目錄中新增一個 「檔案」,並且把檔名改成 .gitignore 。 (切記,是 檔案 ,不是 資料夾 )
  2. 把想忽略的「檔名 (含副檔名)」一個一個寫在裡面就好了。

舉例來說,目前工作目錄中的檔案都是剛新增的檔案,是 Git 「尚未追蹤」 的狀態:
新增檔案示意

如果我想忽略 data.log 以及 app.log ,就直接按照上述步驟操作 (多個檔案「換行」寫入即可):
新增資料示意圖
在 VSCode 中,工作目錄如果看到檔案呈現灰色,則代表 Git 正在忽略這個檔案。

只要把 .gitignore 字樣也寫進 .gitignore 裡面,這個檔案也會被忽略:
.gitignore 被忽略

不知道讀者有沒有從截圖中注意到,此時的 .gitignore 並沒有被加入版控 (也就是尚未被 commit)?

其實只要工作目錄裡面有 .gitignore ,Git 就會自動去讀取它的內容,並且自動忽略符合內容的檔案。 即使 .gitignore 檔案沒有被版控也會有效果!

不過要設定成功必須達成兩個條件:

  1. .gitignore 的內容必須符合撰寫規則 (後續會詳細說明)。
  2. 想忽略的檔案 不能 被紀錄在「索引」內。

第二點尤其重要,如果檔案已經被加入索引 (就是暫存區),甚至已經被 commit 到儲存庫中 (註 1),那麼無論怎麼在 .gitignore 裡面寫資料,Git 都沒辦法排除這個檔案。

就像你已經認識了一個人,結果有人告訴你:「別認識他,忘掉他吧!」
常理來說是很難做到的吧…

註 1. 觀念重申:已經被 commit 的檔案,在索引 (暫存區) 中也會有資料。

# .gitignore 格式

有個重要的事情先講:
.gitignore 中可以寫註解,不過如果要寫註解,「不可以」寫在跟檔名同一行,否則 Git 會把你想寫的註解視為檔名,導致忽略失效。

為了讓大家可以比較好瀏覽,「下方註解故意寫在『無效的位置』」 ,請各位千萬注意!!!

  1. 完整檔案名稱:
.env            # 環境設定檔
.config         # 配置檔案
.DS_Store       # macOS Finder 產生的檔案
app.log         # 名叫 app 的 log 檔案
  1. 直接定義要忽略的副檔名:
*.log           # 忽略所有以 .log 結尾的檔案
*.tmp           # 忽略所有以 .tmp 結尾的檔案
  1. 以驚嘆號 (!) 來例外某些檔案:
*.log           # 忽略所有以 .log 結尾的檔案
!important.log  # 但不忽略 important.log
  1. 忽略任何叫某個名字的檔案 (不管副檔名)
hello.*         # 忽略任何叫 hello 的檔案,包含 hello.txt、hello.java ...
  1. 忽略所有以某個字樣開頭的資料
image*          # 忽略所有以 image 開頭命名的資料 (包含檔案與目錄)
  1. 只忽略「根目錄」的目錄或檔案:
/Test           # 只忽略根目錄 Test ,不忽略子目錄的 Test,例如 Vue/Test 就不會被忽略
  1. 忽略目錄底下的 「所有內容」:
.vscode/        # Visual Studio Code 設定檔
.idea/          # IntelliJ IDEA 設定檔
node_modules/   # Node.js 的依賴模組
build/          # 編譯或建置的結果目錄
dist/           # 發布版程式碼的目錄
  1. 註解 (因為太重要,所以特別寫下來)
# 這是一個有效註解
.vscode/  # 寫在這裡是無效註解,所以上述範例註解會直接使設定無效!!

以上大概是 .gitignore 常見的寫法,如果還有特殊需求,請各位讀者參考 Git 官網 對於設定的說明。

# 設定全域 .gitignore

如果覺得在不同的工作目錄設定固定的「忽略檔案」很麻煩,可以直接設定「全域」的 .gitignore

執行步驟如下:

  1. 在終端機執行
git config --global core.excludesfile "~/.gitignore_global"

或是直接在 .gitconfig[core] 底下加這行,跟執行指令意思一樣 (註 1)。

[core]
	excludesfile = ~/.gitignore_global
  1. 根據不同系統,新增 .gitignore_global 檔案 (註 2):
    Mac 請到這個路徑設定: ~/.gitignore_global
    Windows 在這個路徑: C:\Users\你的使用者名字\.gitignore_global

  2. .gitignore_global 設定的內容,即為「全域 .gitignore 」。

註 1. 如果不知道 .gitconfig 檔案如何操作 ,可以參考 Git 初始化設定文章 常見問題 「第四題」

註 2. 上述的「檔名」與「路徑」都可以自己定義,只要第 1、2 步有辦法對應即可,上面路徑設定只是為了把 全域 .gitignore .gitconfig 放在一樣的路徑。

# 如何忽略已經被版控的檔案?

開頭有提到,已經被版控的檔案會在 索引 (暫存區) 存有資料,所以會導致 .gitignore 無法忽略。

要忽略已被版控的檔案,有三件事情要做 (註 1):

  1. .gitignore 內設定要忽略的檔名。
  2. 移除暫存區中的檔案紀錄。
  3. 執行 git commit 提交一個版本。

可能會有讀者疑惑第三步的動作,明明沒有編輯檔案,是要 commit 什麼東西啊,而且難道不用先 git add 嗎?!

來說明一個小觀念,讓大家細細品嘗 (?):

  • 把已經 commit 的檔案移出暫存區,是一個 行為
  • 資料被移除了,是一個 狀態

在 Git 的世界中,只要有 操作工作目錄或是索引 的「行為」,都會導致「資料狀態」被改變。

資料原本 commit 好的 穩定 狀態如果被改變了,Git 就會認為應該要把這個「被改變的狀態」給記錄起來。

資料:兄弟阿~挖應該嘎你喜無冤無仇啦齁,林北 commit 嘎 喝ㄙㄟˋ喝ㄙㄟˇ ,你無逮無治嘎挖亂叮噹…

我們在執行 git add 時,會把「新增或編輯的檔案狀態」加入索引。
這裡第二步的行為,則是把「移除索引的檔案狀態」加到索引。 (<= 這句話應該最難懂)

兩者在 Git 看來,都是有東西 被加到暫存區 的行為,所以下一步,就可以直接執行 commit 了!

詳細指令操作如下:

  1. 修改 .gitignore ,並視需求決定是否跟著「第三步」一起 commit。

  2. 執行下列指令 (註 2),把檔案移出索引。

git rm --cached 檔名
  1. 直接 commit 提交一個版本 (註 3)。
git commit -m "將 XXX 移出「暫存區」"

如果都順利完成,此時的檔案應該已經能被 Git 忽略了!

註 1. 上述內容也可以在 官網 找到說明。

註 2. rm (remove) 用來告訴 Git 要刪掉檔案, --cached 則是告訴 Git 不是要從「工作目錄」中刪除,只要刪除「索引」的資料就好。

註 3. 再說明一次:透過 git 的指令將資料「移出索引」的行為,資料被移出 的狀態會被加到「暫存區」,所以可以直接 Commit。

# 可能不太重要的觀念補充:把檔案移除索引後的狀態…

我們已經知道 git rm --cached 檔名 是把檔案「移出索引」的指令。
不過執行完指令之後的檔案,有沒有寫進 .gitignore git status 出現的資料狀態會不一樣哦!

舉例來說,假設我忘記把 data.log 放到 .gitignore 就 commit 了,現在想把它移除:
data.log

於是我執行了這個指令:

git rm --cached data.log
  • 「沒有」把 data.log 寫進 .gitignore 時,執行 git status 會出現這樣的資料:
    尚未寫資料示意
    data.log 「可被提交」 區域呈現 「已刪除」 狀態 (綠字那筆),
    同時也會出現在 「未追蹤檔案」 區域 (紅字那筆)。

  • data.log 寫進 .gitignore ,再執行一次 git status
    已經寫資料示意
    神奇的事情出現了, data.log 竟然從 「未追蹤檔案」 區域消失了!!

給大家 10 秒鐘思考是什麼原因。好時間到!

記得「未追蹤」檔案的定義嗎?

只要不在索引 (暫存區) 內的檔案,就是「未追蹤」檔案。

由於 git rm --cached 就是「把檔案從索引移除」的指令,執行完指令後,檔案當然就會是未追蹤狀態。

要是未追蹤的檔案再度被寫到 .gitignore 中,就會讓 Git 會自動「忽略」這個檔案,就好像 Git 「看不到」這個檔案一般,既然對 Git 來說檔案根本就不存在,那也不用探討追蹤與否的問題了。

不過此時的 Git 確實知道有一個叫 data.log 被「移出索引」,而且移出索引的「狀態」還沒有被「紀錄」,所以 git status 會顯示有個已經被刪除的檔案,而且它還沒被 commit 。

我們因此能得知,雖然 .gitignore 不是索引,不過它也會影響到 Git 讀取專案檔案的狀態。

.git/index 索引 (a.k.a. 暫存區) 用來紀錄要讓 Git 「追蹤」的資料;
.gitignore 用來紀錄要讓 Git 「忽略」的資料。

# 常見問題

# 1. git rm --cached 移出檔案,會影響之前 commit 的檔案嗎?

不會。

用另一個角度來看這個指令,執行完指令之後的檔案,就像剛跟 Git 談完分手一樣,兩者變成了最熟悉的陌生人。
他們之間經歷的回憶,就像已經 commit 的內容一樣,Git 不會輕易忘記。

這樣的模式也很合理,commit 行為在 Git 的世界應該算是最神聖的事情,這會把關鍵的時間點封裝成重要的記憶。
如果有任何一個指令,可以一口氣把歷史中 commit 過的某個檔案一次清除,可能也就失去了「版控」的意義。

所以 git rm --cached 依然是一個讓時間「往前走」的指令,他只是某個檔案跟 Git 分手的時刻,但過去被記錄的點點滴滴,不會輕易的被 Git 所遺棄 。

這樣說起來,執行完指令後那個 commit 的行為,就是要發文紀錄一下自己分手了嗎?!

奇怪… 怎麼寫完這段話… 眼角微微泛淚…

是洋蔥,我加了洋蔥

# 2. .gitignore 需要被加入版控嗎?

又是那個老答案:「看需求」。

如果你使用 Git 都是一個人在版控,而且每個工作目錄需要忽略的檔案大同小異,那其實只要建立好 「全域 .gitignore 」 就可以,不一定要在每個專案都定義 .gitignore ,就也不會有要不要把 .gitignore 加入版控的問題了。

不過如果是跟團隊合作某個專案,為了讓整個團隊能擁有一致的「忽略清單」,這種情況就會建議把 .gitignore 加到版控中。
這樣的好處是,如果團隊的專案因為某個需求需要增加「要被忽略的檔案」,只要有人在專案的 .gitignore 檔案定義好,所有人同步之後就搞定了,省得每個人還要額外設定。