iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Software Development

深入一點點認識 Git系列 第 25

Day 25-深入一點點認識 Git:對 commit 的任何更動其實都是在做新 commit

  • 分享至 

  • xImage
  •  

在 Day 24 文章中,我們探討了 git rebase 是如何「看似」把一個分支的 commit 搬到另一分支,但其實 git 內部做的事情是做出「新的 commit」。

在本篇文章中,我們還將探討使用 git rebase 的互動模式(interactive mode)更改 commit 細節,透過觀察 .git/ 資料夾在輸入每個指令後的變化,看 git 在幕後是怎麼幫我們完成工作的。

前置準備

我們直接做好三個 commit。

第一個 commit:

echo "Version 1" > file.txt
git add file.txt
git commit -m "Commit 1"

第二個 commit:

echo "Version 2" > file.txt
git add file.txt
git commit -m "Commit Two"

第三個 commit:

echo "Version 3" > file.txt
git add file.txt
git commit -m "Commit Three"

這時候的 .git/ 資料夾結構如下:

.git/
├── ...
│
├── objects/
│   ├── 0c/
│   │   └── 199340b5d08f2ffbee96e34fb36d900faf3c4e
│   ├── 2b/
│   │   └── 3c8b2a21970adf332d4a3b1bc8b755a4553011
│   ├── 07/
│   │   └── 9c900b81f445bbaa8dde29948583f0a33f50ba
│   ├── 17/
│   │   └── ed90ba28b5b8afea6ccef0f3be1e96e3599107
│   ├── cb/
│   │   └── 4596844ced0a93021cffb2905550167db19bc0
│   ├── da/
│   │   └── 03eee4725ba959a16a02ce41d1289110ef160d
│   ├── ed/
│   │   └── b9794c2dc58736d3d2f87ffee717569a3ed833
│   ├── f4/
│   │   └── 4f8c6b9a98205af69015113a3b7fdda0962e5b
│   ├── fb/
│   │   └── 8247c7b27ae4cad9e7e3e66ba95126658ea7c2
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main    # 指向2b3c8b2a21970adf332d4a3b1bc8b755a4553011
│   └── tags/
│
├── COMMIT_EDITMSG  # 寫著Commit Three
├── ...
└── index

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513RKhCIAeJfA.png

以更動 commit 訊息為例

這分為兩種情況,一是更改「最新的」commit 訊息、二是更改「以前的」commit 訊息。

更改「最新的」commit 訊息

如果只是要改最新的 commit 訊息,就還不需要用到 git rebase,只要用 git commit --amend 就行。

例如在本範例中,要更改最新的 commit 訊息,從 "Commit Three" 變 "Commit 3",就可以先下以下指令:

git commit --amend

這時候會跳出如 Vim 之類的編輯器,顯示以下訊息:

Commit Three

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sat Aug 30 15:13:37 2025 +0800
#
# On branch main
# Changes to be committed:
# modified:   file.txt
#

只要把最上方的 "Commit Three" 改成 "Commit 3"

Commit 3

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sat Aug 30 15:13:37 2025 +0800
#
# On branch main
# Changes to be committed:
# modified:   file.txt
#

然後存檔、退出編輯器,就更改完成了,這時終端機畫面如下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513yBhYwCvezV.png

那如果再觀察 .git/ 資料夾呢?

.git/
├── ...
│
├── objects/
│   ├── 0c/
│   │   └── 199340b5d08f2ffbee96e34fb36d900faf3c4e
│   ├── 2b/
│   │   └── 3c8b2a21970adf332d4a3b1bc8b755a4553011
│   ├── 5c/                                           # 新增的commit物件
│   │   └── d5083719f126193fcf153470afe76aa070d6e5
│   ├── 07/
│   │   └── 9c900b81f445bbaa8dde29948583f0a33f50ba
│   ├── 17/
│   │   └── ed90ba28b5b8afea6ccef0f3be1e96e3599107
│   ├── cb/
│   │   └── 4596844ced0a93021cffb2905550167db19bc0
│   ├── da/
│   │   └── 03eee4725ba959a16a02ce41d1289110ef160d
│   ├── ed/
│   │   └── b9794c2dc58736d3d2f87ffee717569a3ed833
│   ├── f4/
│   │   └── 4f8c6b9a98205af69015113a3b7fdda0962e5b
│   ├── fb/
│   │   └── 8247c7b27ae4cad9e7e3e66ba95126658ea7c2
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main   # 指向5cd5083719f126193fcf153470afe76aa070d6e5
│   └── tags/
│
├── COMMIT_EDITMSG # 寫著Commit 3與其他剛剛在編輯器中看到的文字
├── ...
└── index

竟然多了一個 commit 物件 5cd5083...,再用 git cat-file -p <commit> 檢視會發現目前關係圖如下:
https://ithelp.ithome.com.tw/upload/images/20250924/20178513H4e3lB8sSk.png

更改「更早的」commit 訊息

如果要改的 commit 不是最新者,就是 git rebase 互動模式的出場時機。

假設我們要更改第二個 commit cb45968...,就先輸入以下指令:

git rebase -i da03eee

此時將跳出 Vim 之類的編輯器,顯示以下訊息:

pick cb45968 Commit Two
pick 5cd5083 Commit 3

# Rebase da03eee..5cd5083 onto da03eee (2 commands)
#
# 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
#
# 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.
#

最上面有歷次 commit 資訊,但跟 git log --oneline 順序相反,越下面的 commit 越新。

在這段時間,.git/ 資料夾會出現一個 rebase-merge/ 資料夾與 AUTO_MERGE 參考如下:

.git/
├── ...
│
├── rebase-merge/
│   ├── git-rebase-todo            # 編輯器裡面顯示的內容
│   ├── git-rebase-todo.backup
│   ├── head-name                  # refs/heads/main 
│   ├── interactive                # 內容為空
│   ├── no-reschedule-failed-exec  # 內容為空
│   ├── onto                       # da03eee4725ba959a16a02ce41d1289110ef160d
│   └── orig-head                  # 5cd5083719f126193fcf153470afe76aa070d6e5
│
├── AUTO_MERGE                     # 內容為079c900b81f445bbaa8dde29948583f0a33f50ba
├── ...
└── index

目前 commit 前都是接 pick,但我們現在要做的是:修改 commit 訊息而不更動 commit 其他內容。編輯器下方內容寫著這樣的提示文字:

# r, reword <commit> = use commit, but edit the commit message

因此把要改的 commit cb45968... 前的 pick 改成 rewordr,這邊先用全稱 reword 如下:

reword cb45968 Commit Two

接著存檔、關閉編輯器,就會再跳出一次編輯器,內容如下:

Commit Two

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sat Aug 30 15:13:23 2025 +0800
#
# interactive rebase in progress; onto da03eee
# Last command done (1 command done):
#    reword cb45968 Commit Two
# Next command to do (1 remaining command):
#    pick 5cd5083 Commit 3
# You are currently editing a commit while rebasing branch 'main' on 'da03eee'.
#
# Changes to be committed:
# modified:   file.txt
#

這時 .git/rebase-merge/ 資料夾變成:

.git/
├── ...
│
├── rebase-merge/
│   ├── author-script # GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_AUTHOR_DATE
│   ├── done # reword cb4596844ced0a93021cffb2905550167db19bc0 Commit Two
│   ├── end  # 2
│   ├── git-rebase-todo            # 舊有檔案,但內容變成pick 5cd5083719f126193fcf153470afe76aa070d6e5 Commit 3
│   ├── git-rebase-todo.backup
│   ├── head-name                  # 舊有檔案,內容仍為refs/heads/main 
│   ├── interactive                # 舊有檔案,內容為空
│   ├── msgnum                     # 新檔案,內容為1
│   ├── no-reschedule-failed-exec  # 舊有檔案,內容為空
│   ├── onto                       # 舊有檔案,內容仍為da03eee4725ba959a16a02ce41d1289110ef160d
│   └── orig-head                  # 舊有檔案,內容仍為5cd5083719f126193fcf153470afe76aa070d6e5
│
├── ...
└── index

把編輯器最上方的 commit 訊息 "Commit Two" 改成 "Commit 2"

Commit 2

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sat Aug 30 15:13:23 2025 +0800
#
# interactive rebase in progress; onto da03eee
# Last command done (1 command done):
#    reword cb45968 Commit Two
# Next command to do (1 remaining command):
#    pick 5cd5083 Commit 3
# You are currently editing a commit while rebasing branch 'main' on 'da03eee'.
#
# Changes to be committed:
# modified:   file.txt
#

接著存檔、把編輯器關掉,完成 commit 修改:
https://ithelp.ithome.com.tw/upload/images/20250925/20178513Hx8sYe4YWw.png

這時候 .git/ 資料夾長這樣:

.git/
├── ...
│
├── objects/
│   ├── 0c/
│   │   └── 199340b5d08f2ffbee96e34fb36d900faf3c4e
│   ├── 2b/
│   │   └── 3c8b2a21970adf332d4a3b1bc8b755a4553011
│   ├── 5c/
│   │   └── d5083719f126193fcf153470afe76aa070d6e5
│   ├── 07/
│   │   └── 9c900b81f445bbaa8dde29948583f0a33f50ba
│   ├── 17/
│   │   └── ed90ba28b5b8afea6ccef0f3be1e96e3599107
│   ├── 45/                                        # 新增的commit物件
│   │   └── 37f300f8920896d53dac24887012bd54d96787
│   ├── cb/
│   │   └── 4596844ced0a93021cffb2905550167db19bc0
│   ├── da/
│   │   └── 03eee4725ba959a16a02ce41d1289110ef160d
│   ├── de/                                        # 新增的commit物件
│   │   └── f9a0ef6ec338cce3ace0dfd456d0e9e9dd44f5
│   ├── ed/
│   │   └── b9794c2dc58736d3d2f87ffee717569a3ed833
│   ├── f4/
│   │   └── 4f8c6b9a98205af69015113a3b7fdda0962e5b
│   ├── fb/
│   │   └── 8247c7b27ae4cad9e7e3e66ba95126658ea7c2
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main  # 指向def9a0ef6ec338cce3ace0dfd456d0e9e9dd44f5
│   └── tags/
│
├── AUTO-MERGE    # 079c900b81f445bbaa8dde29948583f0a33f50ba
├── COMMIT_EDITMSG  # 寫著Commit 2與其他剛剛在編輯器中看到的文字
├── ...
└── index

整體物件結構圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250925/2017851374BnC4Rhon.png

雖然這次我們改的是寫著 "Commit Two" 的物件,但連寫著 "Commit Three" 的物件也都跟著跑出新的,可見更新一個特定 commit 會連帶影響從它長出來的所有 commit。

但是如果用 git log 觀察,則會忽略那些被懸置的舊物件,僅保留目前仍有用的 commit:
https://ithelp.ithome.com.tw/upload/images/20250925/20178513nTzUxrm7yH.png

現在我們知道,「更改 commit 訊息」並沒有想像中簡單,不是更改舊的 commit 物件訊息,而是會做出新的 commit 物件。

延伸說明

其實透過 git rebase 的互動模式還可以做更多事情,例如:

  1. 合併 commit
  2. 拆開 commit
  3. 刪除 commit
  4. 更改 commit 順序
  5. 在兩 commit 之間塞新的 commit

這所有操作都會生成新的 commit 物件!所以每完成一個操作,跑出來的 commit 內容(commit 訊息、commit 者資訊等)或許一樣,但雜湊碼跟 commit 建立時間都是不同的,有時甚至連指向的 tree 物件都會被重做出來,這確保了已經被建立的物件不會被更改,如果之後發現此操作有誤,可再用 git reflog 檢視歷史紀錄再救回。

小結

git rebase 的互動模式可以對 commit 做更細緻的操作,但不管是更改 commit 訊息、做新的 commit、合併或拆開 commit、更改 commit 順序等,都不是在原本的 commit 上更動,而是做出內容與舊 commit 相同的新 commit,新的歷史紀錄再都改指向這些新 commit,使舊 commit 變成懸置物件。

參考資料

  1. 7.6 Git Tools - Rewriting History
  2. 【狀況題】修改歷史訊息

上一篇
Day 24-深入一點點認識 Git:git rebase 不是直接把一分支上的 commit 搬到另一分支
下一篇
Day 26-深入一點點認識 Git:git filter-branch 可以改寫歷史,但還不足以完全清除檔案紀錄
系列文
深入一點點認識 Git27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言