學習 Git (6) - 修改 commit 紀錄 part 3:Rebase


Posted by Calon on 2022-05-16

除了前面兩種方式之外,第三種方式是使用 git rebasegit rebase 和前面兩種方式不同的地方在於可以更靈活地去根據需求來修改 commit。
首先我們來看一下 commit:

$ git log --oneline
ffc541b (HEAD -> master) 新增 work 1
c8ba232 新增 works.txt
aa29fdd 新增 rule 2
feff33e 新增 rule 1
1551d95 init

接下來我們使用 git rebase

$ git rebase -i 1551d95

git rebase 後面加上參數 -i 會進入互動模式,而 1551d95 則是想要修改到哪裡,也就是要修改的範圍為「現在(ffc541b)到 1551d95」。

輸入之後會跳出 Vim 編輯器:

pick feff33e 新增 rule 1
pick aa29fdd 新增 rule 2
pick c8ba232 新增 works.txt
pick ffc541b 新增 work 1

# Rebase 1551d95..ffc541b onto ffc541b (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop = 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.
#
# Note that empty commits are commented out

最上面的是我們的 commit 紀錄,順序和 git log 是相反的,從上到下是最舊到最新的 commit。
下面的 Commands 是我們等下要使用的指令,上面的 commit 前面的 pick 就是其中一個指令,意思是「保留此 commit,不做任何修改」。

等下會用到 Vim 來編輯,這邊稍微介紹 Vim 的操作:

  • h, j, k, l:分別對應「左、下、上、右」
  • i:進入輸入模式,此模式下可以輸入文字,移動要切回普通模式
  • esc:回到普通模式
  • v:進入可視模式,可以選取文字,移動方式使用 hjkl
  • x:刪除文字,普通模式下會刪除光標所在的文字,可視模式會刪除所選取的文字
  • :wq:在普通模式下輸入,會儲存並離開編輯器

修改歷史紀錄

在這邊我想要對 c8ba232 新增 works.txtpick ffc541b 新增 work 1 這兩個 commit 的訊息進行修改,所以要將這兩行前面的 pick 改成 reword 或是 r:

pick feff33e 新增 rule 1
pick aa29fdd 新增 rule 2
reword c8ba232 新增 works.txt
reword ffc541b 新增 work 1

修改完後儲存離開,這時會跳出令一個 Vim 編輯器的畫面:

新增 works.txt

# ...略

這是 c8ba232 的訊息內容,現在我們可以來對它進行修改,在這邊我將 works 改成 jobs。
而在修改完之後一樣儲存離開後會發現又跳出另一個 Vim 編輯器,還記得我們要修改兩個 commit 嗎?這就是第二個 commit 的內容:

新增 work 1

# ...略

這裡的 work 也改成 job,好了之後儲存離開,Git 會開始執行剩下的工作:

[detached HEAD a68373d] 新增 jobs.txt
 Date: Mon May 16 13:56:42 2022 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 works.txt
[detached HEAD 8792769] 新增 job 1
 Date: Mon May 16 13:57:13 2022 +0800
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/master.

有注意到 commit 的 SHA-1 值不一樣了嗎?

使用 git rebase 修改完上面的 commit 後,現在使用 git log --oneline 來看一下有沒有修改成功:

$ git log --oneline
8792769 (HEAD -> master) 新增 job 1
a68373d 新增 jobs.txt
aa29fdd 新增 rule 2
feff33e 新增 rule 1
1551d95 init

有發現剛才最上面兩個 commit 的 SHA-1 值不一樣了嗎?還記得在 學習Git(5)_-_檢視_commit_紀錄.md 有提到 Git 產生物件的時候會根據時間、內容等來計算 SHA-1 值,而我們剛才修改了 commit 的訊息,並且時間也不一樣,所以計算出來的 SHA-1 值也會跟原本的不同。

而在之前介紹的 git commit --amend -mgit reset 也是一樣修改之後的 commit 的 SHA-1 都會跟原本的不同。

想取消剛剛的 Rebase

如果在執行完 git rebase 後又不想要剛才的修改的話,我們可以使用 git reset 回到 ORIG_HEAD:

$ git reset ORIG_HEAD --hard

而 ORIG_HEAD 是什麼呢?
當我們在進行一些有風險的操作時,例如:reset、rebase 或合併分支的 merge 時,Git 會將 HEAD(HEAD 之後在介紹分支時會詳細介紹)指向的 commit 物件的 SHA-1 值存放在 .git/ORIG_HEAD 檔案裡,我們可以使用 cat 指令來看裡面的內容:

$ cat .git/ORIG_HEAD
feff33e13511c41fbb8681f56e92945e380bcdc8

對應前面的 commit 紀錄會是指 feff33e 新增 rule 1,也就是使用 git reset ORIG_HEAD --hard 時會回到 feff33e 這個 commit 的狀態。
而使用 git reset ORIG_HEAD --hard 對 Git 來說也是一個危險操作,所以如果恢復到執行 git reset ORIG_HEAD --hard 之前的狀態時只要再次執行一樣的指令即可。

合併 Commit

這次我想要將關於 rule 的 commit 都合併在一起,一樣先使用 git rebase

$ git rebase -i 1551d95

接著會跳出 Vim 編輯器畫面:

pick feff33e 新增 rule 1
pick aa29fdd 新增 rule 2
pick a68373d 新增 jobs.txt
pick 8792769 新增 job 1

...略

現在我們要將 feff33e 和 aa29fdd 這兩個 commit 合併在一起,所以要將 aa29fdd 前面的 pick 改成 squash 或是 s,squash 是「將此 commit 合併到前一個 commit」,要注意 git rebase 的順序和 git log 是相反的,所以是將 aa29fdd 的改成 squash。

儲存離開之後,跟剛才修改 commit 時一樣會再跳出 Vim 編輯器畫面:

# This is a combination of 2 commits。
# This is the 1st commit message:

新增 rule 1

# This is the commit message #2:

新增 rule 2

...略

將不用的 commit 訊息刪除,並修改成 「新增 all rules」:

# This is a combination of 2 commits。
# This is the 1st commit message:

新增 all rules

儲存離開之後,Git 就會繼續執行:

[detached HEAD 1d15ca4] 新增 all rules
 Date: Mon May 16 13:54:39 2022 +0800
 1 file changed, 2 insertions(+)
Successfully rebased and updated refs/heads/master.

我們也可以同時合併多個 commit:

pick feff33e 新增 rule 1
squash aa29fdd 新增 rule 2
squash a68373d 新增 jobs.txt
squash 8792769 新增 job 1

...略

儲存離開之後,跳出 Vim 編輯器,這次可以看到有 4 個 commit 訊息:

# This is a combination of 4 commits。
# This is the 1st commit message:

新增 rule 1

# This is the commit message #2:

新增 rule 2

# This is the commit message #3:

新增 jobs.txt

# This is the commit message #4:

新增 job 1

...略

我們將訊息修改為「新增 rules 和 jobs」:

# This is a combination of 4 commits。
# This is the 1st commit message:

新增 rules 和 jobs

儲存離開後我們用 git log --oneline 來看 commit 有沒有合併成功:

[detached HEAD 28ce6b7] 新增 rules 和 jobs
 Date: Mon May 16 13:54:39 2022 +0800
 2 files changed, 3 insertions(+)
 create mode 100644 works.txt
Successfully rebased and updated refs/heads/master.
$ git log --oneline
28ce6b7 (HEAD -> master) 新增 rules 和 jobs
1551d95 init

發現剛才 4 個都合併一起並產生 1 個 28ce6b7 的新 commit。

拆解之前的 commit

我們先來看一下 commit:

$ git log --oneline
0a4944c (HEAD -> master) 新增 job 1
f609649 新增 jobs.txt
3f0d4b9 新增 rules
deb6d18 init

d86bb1a 這個 commit 一次新增太多了,我想要個拆成一個 commit,這時一樣先使用 git rebase

$ git rebase -i deb6d18

跳出的 Vim 編輯器畫面:

pick 3f0d4b9 新增 rules
pick f609649 新增 jobs.txt
pick 0a4944c 新增 job 1

...略

這次我們使用 edit 這個指令,這會讓 Git 在執行到指定的 commit 時會停止:

edit 3f0d4b9 新增 rules
pick f609649 新增 jobs.txt
pick 0a4944c 新增 job 1

接下來 Git 在執行到 d86bb1a 時就會停下,我們可以使用 git log 來看一下:

$ git rebase -i deb6d18
Stopped at 3f0d4b9...  新增 rules
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue
$ git log --oneline
3f0d4b9 (HEAD) 新增 rules
deb6d18 init

0a4944c 和 f609649 不見了,並且 3f0d4b9 旁邊出現 HEAD,代表 HEAD 現在指向 3f0d4b9。
現在要來拆掉 3f0d4b9,可以使用之前介紹過得 git reset

$ git reset HEAD^

還記得 ^ 是什麼意思嗎?是指前一個 commit,而 HEAD^ 就是指回到 HEAD 指向的 commit 的前一個 commit,也就是 deb6d18,如果忘記 git reset 的用法可以回去參考學習 Git (6) - 修改 commit 紀錄 part 2:_Reset

執行完 git reset 後,接下來使用 git status 來檢查狀態:

$ git status
interactive rebase in progress; onto deb6d18
Last commands done (1 commands done):
   edit 3f0d4b9 新增 rules
Next commands to do (2 remaining commands):
   pick f609649 新增 jobs.txt
   pick 0a4944c 新增 job 1
  (use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch 'master' on 'deb6d18'.

Changes not statged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified: rules.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
       rule2.txt

發現 rules.txt 和 rule2.txt 已經被拆出來放到暫存區了,然後分別對這 2 個檔案執行 git addgit commit

  • 提交 rules.txt:

    $ git add rules.txt
    $ git commit -m '新增 rules.txt 內容'
    [detached HEAD 7cf2baa] 新增 rules.txt 內容
    1 file changed, 1 insertion(+)
    
  • 提交 rule2.txt:

    $ git add rule2.txt
    $ git commit -m '新增 rule2.txt'
    [detached HEAD c8699fa] 新增 rule2.txt
    1 file changed, 0 insertions(+), 0 deletions(-)
    create mode 100644 rule2.txt
    

最後執行 git rebase --continue 讓 Git 繼續執行接下來的動作:

$ git rebase --continue
Successfully rebased and updated refs/heads/master.

git log 來看一下有沒有成功:

$ git log --oneline
ce85586 (HEAD -> master) 新增 job 1
65695a6 新增 jobs.txt
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

發現 3f0d4b9 不見了,並新增了剛剛提交的 7cf2baa 和 c8699fa 這樣就成功將「新增 rules」這個 commit 拆成 2 個 commit 囉。

想刪除某個 commit 或是調整 commit 位置

最後要來介紹怎麼刪除 commit 或是調整 commit 位置,我們沿用上面拆解 commit 範例:

$ git log --oneline
ce85586 (HEAD -> master) 新增 job 1
65695a6 新增 jobs.txt
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

我們在這邊新增一個 rule3.txt,並提交 commit 上去:

$ touch rule3.txt
$ git add rule3.txt
$ git commit -m '新增 rule 3'
[master 852a230] 新增 rule 3
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 rule3.txt

完成之後,commit 紀錄會多出一個「新增 rule 3」:

$ git log --oneline
852a230 (HEAD -> master) 新增 rule 3
ce85586 新增 job 1
65695a6 新增 jobs.txt
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

這時我想要將 rule 相關的 commit 放在一起,一樣可以使用 git rebase

$ git rebase -i deb6d18

執行之後跳出的 Vim 畫面:

pick 7cf2baa 新增 rules.txt 內容
pick c8699fa 新增 rule2.txt
pick 65695a6 新增 jobs.txt
pick ce85586 新增 job 1
pick 852a230 新增 rule 3

接著只要調整 852a230 的位置,要記住在 rebase 中時間順序是和 log 相反的:

pick 7cf2baa 新增 rules.txt 內容
pick c8699fa 新增 rule2.txt
pick 852a230 新增 rule 3
pick 65695a6 新增 jobs.txt
pick ce85586 新增 job 1

修改完後儲存離開,接下來使用 git log 來檢查有沒有成功調換位置:

$ git log --oneline
7f34f07 (HEAD -> master) 新增 job 1
cde5774 新增 jobs.txt
ccf65d1 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

ccf65d1 有移到 c8699fa 後面了。

刪除指定 commit

使用 git rebase 也很簡單,這次我們來刪除 jobs 相關的 commit,這次不會修改到 rules 相關的 commit,所以範圍指定到 ccf65d1:

$ git rebase -i ccf65d1

Vim 的畫面如下:

pick cde5774 新增 jobs.txt
pick 7f34f07 新增 job 1

接下來只要把不要的地方刪掉,或是把 pick 改成 drop:

drop cde5774 新增 jobs.txt

這邊一個使用 drop,一個直接刪除,存檔後離開再使用 git log

$ git log --oneline
ccf65d1 (HEAD -> master) 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

jobs 相關的 commit 都不見了,這樣就刪除成功了。


參考資料
  • 高見龍,《為你自己學 Git》

#Git







Related Posts

[第一週] 認識 Command Line

[第一週] 認識 Command Line

字串小幫手 on linux

字串小幫手 on linux

Android 不負責任系列 - Jetpack 組件、MVVM 架構,簡稱 AAC、整潔架構(Clean Architecture) 的領域層(Domain Layer) UseCase 介紹

Android 不負責任系列 - Jetpack 組件、MVVM 架構,簡稱 AAC、整潔架構(Clean Architecture) 的領域層(Domain Layer) UseCase 介紹


Comments