第 03 章

Commit 提交

Commit 是 Git 的核心操作——每一次 commit 就是為專案拍一張「快照」。本章帶你理解 commit 的概念、內部結構、歷史查閱與版本回溯。

Commit 的概念與用途

Commit(提交)是 Git 中最基本的版本紀錄單位。每一次 commit 會記錄:

📸 完整的專案快照

不是只記錄「哪些行改了」,而是記錄整個專案在該時刻的完整狀態。Git 透過 tree 物件指向所有檔案的 blob。

📝 提交的後設資料

包含作者(Author)、提交者(Committer)、時間戳記、提交訊息(Commit Message),以及父 commit 的指標。

快照 vs 差異:很多人以為 Git 像 SVN 一樣儲存「每次修改的差異(diff)」,但實際上 Git 儲存的是完整的快照。如果某個檔案沒有變更,Git 不會重複儲存,而是重複引用同一個 blob 物件。

git add + git commit 完整流程

要建立一筆 commit,需要兩個步驟:

flowchart LR A["編輯檔案"] --> B["git add
加入暫存區"] B --> C["git commit
建立快照"] C --> D["新的 Commit 物件
寫入 objects/"]

第一次 Commit 的完整操作

# 建立第一個檔案
$ echo "# My Project" > README.md

# 查看狀態
$ git status

# 將檔案加入暫存區
$ git add README.md

# 建立 commit
$ git commit -m "初始提交:新增 README"

# 查看歷史紀錄
$ git log
# git add 之前
$ git status
On branch main
No commits yet
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        README.md

# git add 之後
$ git status
On branch main
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   README.md

# git commit 之後
$ git status
On branch main
nothing to commit, working tree clean
  • Untracked:新建的檔案,Git 還不知道它的存在。
  • Staged:用 git add 把檔案加入暫存區後,Git 知道「下次 commit 要包含它」。
  • Committedgit commit 把暫存區的內容打包成一個快照,寫入物件資料庫。
  • working tree clean:工作目錄和最新 commit 完全一致,沒有未提交的變更。

Commit ID(SHA-1)

每一筆 commit 都有一個唯一的 SHA-1 雜湊值作為 ID。這個 ID 是根據 commit 的完整內容(包含 tree、parent、author、message)計算出來的。

$ git log --oneline
c3a2f1b (HEAD -> main) 新增 style.css
a7d8e9f 新增 index.html
b1c2d3e 初始提交:新增 README

完整 ID
c3a2f1b4e5d6f7...
40 字元

短 ID
c3a2f1b
通常 7 字元就夠用

唯一性
內容不同 → ID 不同
內容相同 → ID 相同

Commit 物件的內部結構

一個 commit 物件包含以下資訊:

用 git cat-file 查看 commit 物件

$ git cat-file -p c3a2f1b
tree 4b825dc642cb6eb9a060e54bf899d31b8fc28e3b
parent a7d8e9f1234567890abcdef1234567890abcdef12
author Alice <alice@example.com> 1710849600 +0800
committer Alice <alice@example.com> 1710849600 +0800

新增 style.css
flowchart TD C["Commit c3a2f1b
新增 style.css"] C --> T["Tree 4b825d...
(專案目錄快照)"] C --> P["Parent a7d8e9f
(上一個 commit)"] T --> B1["blob: README.md"] T --> B2["blob: index.html"] T --> B3["blob: style.css ← 新增"]
欄位說明
tree指向一個 tree 物件,代表這次 commit 時整個專案的目錄結構快照。
parent指向上一個 commit 的 SHA-1。第一筆 commit 沒有 parent;merge commit 有兩個以上的 parent。
author原始程式碼的作者名稱、email 與時間戳記。
committer實際執行 commit 的人(通常和 author 相同,但在 cherry-pick 或 rebase 時可能不同)。
空行後的文字提交訊息(Commit Message)。

Commit 的歷史紀錄與查閱

Git 的提交歷史形成一條鏈狀結構——每個 commit 指向它的 parent,組成一條時間線。

flowchart RL C4["Commit 4
HEAD → main"] --> C3["Commit 3"] C3 --> C2["Commit 2"] C2 --> C1["Commit 1
(初始提交)"]

常用的 git log 參數

指令效果
git log顯示完整的提交歷史(按時間倒序)
git log --oneline每筆 commit 只顯示一行(短 ID + 訊息)
git log --graph用 ASCII 圖形顯示分支結構
git log --oneline --graph --all圖形化顯示所有分支的歷史
git log -n 5只顯示最近 5 筆 commit
git log --stat顯示每筆 commit 修改了哪些檔案
git log -p顯示每筆 commit 的詳細 diff

git diff:比較差異

指令比較對象
git diff工作目錄 vs 暫存區(尚未 add 的變更)
git diff --staged暫存區 vs 最新 commit(已 add 但尚未 commit 的變更)
git diff HEAD工作目錄 vs 最新 commit
git diff a7d8e9f c3a2f1b兩筆 commit 之間的差異

版本回溯

Git 最大的優勢之一就是可以隨時回到過去的版本。以下是幾種常見的回溯方式:

git checkout(查看舊版本)

暫時跳到某個歷史 commit 查看內容,不影響現有分支。

$ git checkout a7d8e9f
# 進入 Detached HEAD 狀態
# 查看完畢後回到分支
$ git checkout main

git restore(還原檔案)

將工作目錄或暫存區的檔案還原到特定版本。

# 還原工作目錄中的檔案
$ git restore index.html

# 取消暫存(unstage)
$ git restore --staged index.html

# 還原到特定 commit 的版本
$ git restore --source=a7d8e9f index.html
⚠️ 注意:git restore 會直接覆蓋工作目錄的檔案,這個操作無法復原。如果有尚未提交的重要修改,請先 commit 或 stash。

git commit 背後做了哪些動作?

當你執行 git commit 時,Git 在內部依序完成以下步驟:

flowchart TD A["git commit -m '...'"] --> B["讀取暫存區 (.git/index)"] B --> C["為每個暫存的檔案建立 blob 物件"] C --> D["建立 tree 物件
(記錄目錄結構)"] D --> E["建立 commit 物件
(指向 tree + parent + metadata)"] E --> F["更新分支指標
refs/heads/main → 新 commit"] F --> G["更新 HEAD
(仍指向 main)"]
重要觀念:Commit 完成後,暫存區不會被清空。暫存區會保持和最新 commit 一致的狀態。只有當你再次 git add 新的變更時,暫存區才會和工作目錄有差異。

撰寫好的 Commit Message

好的提交訊息讓團隊成員(包括未來的你)能快速理解每筆變更的目的。

❌ 不好的訊息

fix bug
update
修改
asdflkj
WIP

✅ 好的訊息

修正登入頁面密碼驗證的錯誤邊界條件
新增使用者註冊的 Email 格式驗證
重構購物車計算邏輯,提取折扣計算為獨立函式
更新 README 安裝說明,補充 Node.js 18+ 需求

Commit Message 格式建議

# 格式
<類型>: <簡短描述>(50 字以內)

<詳細說明>(可選,72 字換行)

# 常見類型
feat:     新功能
fix:      修正 bug
docs:     文件修改
style:    格式調整(不影響邏輯)
refactor: 重構(不改功能也不修 bug)
test:     測試相關
chore:    雜事(建置、設定等)

🔧 動手練習

練習 1:建立多筆 Commit

$ echo "# My Project" > README.md
$ git add README.md
$ git commit -m "feat: 新增 README"

$ echo "<h1>Hello</h1>" > index.html
$ git add index.html
$ git commit -m "feat: 新增首頁"

$ echo "body { margin: 0; }" > style.css
$ git add style.css
$ git commit -m "feat: 新增樣式表"

$ git log --oneline

練習 2:查看 Commit 內部

# 查看最近一筆 commit 的 SHA-1
$ git log --oneline -1

# 用 cat-file 查看 commit 物件
$ git cat-file -p HEAD

# 用 cat-file 查看 tree 物件
$ git cat-file -p HEAD^{tree}

# 比較兩筆 commit 的差異
$ git diff HEAD~2 HEAD

本章小結

知識重點

  • Commit 是專案的完整快照,不是差異檔。
  • 每筆 commit 有唯一的 SHA-1 ID。
  • Commit 物件包含 tree、parent、author、committer 與 message。
  • 提交歷史形成鏈狀結構,可以用 git log 查閱。
  • 版本回溯可用 git checkoutgit restore
  • 撰寫清晰的 Commit Message 是重要的團隊協作習慣。

下一章預告

  • 認識 GitHub 協作平台的核心功能。
  • 學習 Remote(遠端版本庫)的概念。
  • 操作 git pushgit pullgit clone
學習提示: git add + git commit 是你每天都會用到的操作。建議現在就打開終端機,建立一個測試 Repo 並實際操作幾輪,直到你對整個流程感到自然為止。