Git で複数のコミットを1つにまとめられる「スカッシュ」というテクニック

2023.10.06

こんにちは、CX 事業本部 Delivery 部の若槻です。

今回は、Git で複数のコミットをまとめる方法を確認してみました。

ちなみに Git で行うこの操作のことを「スカッシュ(squash)」するとも言います。squash は「押しつぶす」とか「ぺちゃんこにする」という意味だそうです。

環境

$ vim --version
VIM - Vi IMproved 9.0 (2022 Jun 28, compiled Jun 23 2023 22:12:29)
macOS version - arm64
Included patches: 1-1544

確認してみた

スカッシュしたいコミットが「連続する」場合と「連続していない」場合の 2 通りの方法を確認してみました。

連続するコミットの場合

まずは「連続する」複数のコミットをスカッシュする場合の方法です。

スカッシュ前の状態

次のようなコミットログがある。このうち funcB の実装に関する変更である 3 つのコミット e03354caf7c678 および 346de39 を 1 つにまとめたいとします。

$ git log --oneline -5
d8a5fac (HEAD -> main) implement funcC
346de39 typo funcB
af7c678 fix funcB
e03354c implement funcB
f3b0727 implement funcA

スカッシュする

コマンド git rebase -i HEAD~ を実行します。-i--interactive のエイリアスです。

git rebase -i HEAD~5

するとコミットログがインタラクティブモードで開き、リベースできるようになります。

pick f3b0727 implement funcA
pick e03354c implement funcB
pick af7c678 fix funcB
pick 346de39 typo funcB
pick d8a5fac implement funcC

# Rebase ab8e0fe..d8a5fac onto ab8e0fe (5 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
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# 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.
#

キーボードでiを押下し、インサートモードに入ります。インサートモードが有効になるとエディター末尾に-- INSERT --と表示されます。

pick f3b0727 implement funcA
pick e03354c implement funcB
pick af7c678 fix funcB
pick 346de39 typo funcB
pick d8a5fac implement funcC

(中略)
-- INSERT --

スカッシュしたい一連のコミットのうち、最も古いコミット以外のコミットの行頭の picks(または squash)に変更します。

pick f3b0727 implement funcA
pick e03354c implement funcB
s af7c678 fix funcB
s 346de39 typo funcB
pick d8a5fac implement funcC

変更が完了したら ESC キーでインサートモードを抜けて、:wq で保存します。

すると次はコミットメッセージを編集する画面になります。ここでは、スカッシュしたいコミットのメッセージを残して、それ以外のコミットのメッセージは削除します。編集はまたインサートモードで行います。

# This is a combination of 3 commits.
# This is the 1st commit message:

implement funcB

# This is the commit message #2:

fix funcB

# This is the commit message #3:

typo funcB

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sat Oct 7 03:14:46 2023 +0900
#
# interactive rebase in progress; onto ab8e0fe
# Last commands done (4 commands done):
#    squash af7c678 fix funcB
#    squash 346de39 typo funcB
# Next command to do (1 remaining command):
#    pick d8a5fac implement funcC
# You are currently rebasing branch 'main' on 'ab8e0fe'.
#
# Changes to be committed:
#       new file:   funcB.ts
#

編集後の画面です。# で始まる行はコミットメッセージには表示されないので、残したままでも問題ありません。

# This is a combination of 3 commits.
# This is the 1st commit message:

implement funcB

# This is the commit message #2:

# This is the commit message #3:

変更が完了したら ESC キーでインサートモードを抜けて、:wq で保存します。

スカッシュ後の状態

確認のためコミットログを表示します。funcB の実装に関する変更である 3 つのコミットが 1 つにまとめられていることが分かります。またスカッシュされたコミットおよびスカッシュ後のコミットのハッシュ値は変わっています。

$ git log --oneline -3
64daa2c (HEAD -> main) implement funcC
972f682 implement funcB
f3b0727 implement funcA

スカッシュされたコミットの内容を確認します。funcB の実装に関する変更である 3 つのコミットが 1 つにまとめられていることが分かります。(変更内容はかなり適当です。)

$ git show 972f682
commit 972f68222885a7558563db9a948f6c013e9feddb
Author: cm-rwakatsuki <wakatsuki.ryuta@classmethod.jp>
Date:   Sat Oct 7 03:14:46 2023 +0900

    implement funcB

diff --git a/funcB.ts b/funcB.ts
new file mode 100644
index 0000000..d03c6d8
--- /dev/null
+++ b/funcB.ts
@@ -0,0 +1,2 @@
+() => {};
+() => {};

連続していないコミットの場合

次に、「連続していない」複数のコミットをスカッシュする場合です。と言っても連続した場合と比べてそんなに特別な操作は必要ありません。

スカッシュ前の状態

例えば、次のようなコミットログのうち、間に別のコミットを挟んでいる 64daa2cf3b0727 をスカッシュしたいとします。

$ git log --oneline -3
64daa2c (HEAD -> main) implement funcC
972f682 implement funcB
f3b0727 implement funcA

スカッシュする

先ほどと同様に、コマンド git rebase -i HEAD~ を実行します。

git rebase -i HEAD~3

リベースのインタラクティブモードが開かれます。

pick f3b0727 implement funcA
pick 972f682 implement funcB
pick 64daa2c implement funcC

開いたエディターでインサートモードに入ったら、次のようにスカッシュしたいコミットが連続するように並べ替えます。また先ほどと同様に最も古いものを除くスカッシュしたいコミットの行頭の picks(または squash)に変更します。

pick 972f682 implement funcB
pick f3b0727 implement funcA
s 64daa2c implement funcC

変更が完了したら ESC キーでインサートモードを抜けて、:wq で保存します。

ここからも同じです。コミットメッセージを編集する画面になるので、スカッシュ後のメッセージを記載します。

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

implement funcA

# This is the commit message #2:

implement funcC

変更後のコミットはこのようにしてみました。

implement funcA funcC

変更が完了したら ESC キーでインサートモードを抜けて、:wq で保存します。

スカッシュ後の状態

コミットログの確認。指定したコミットメッセージの新しいコミットにスカッシュされていることが分かります。

$ git log --oneline -2
7771112 (HEAD -> main) implement funcA funcC
22fe013 implement funcB
ab8e0fe fix

コミットの内容も期待どおりです。

$ git show 7771112
commit 7771112b3d51a1c22c784e8a9f9b28c520cc4756 (HEAD -> main)
Author: cm-rwakatsuki <wakatsuki.ryuta@classmethod.jp>
Date:   Sat Oct 7 03:14:29 2023 +0900

    implement funcA funcC

diff --git a/funcA.ts b/funcA.ts
new file mode 100644
index 0000000..e69de29
diff --git a/funcC.ts b/funcC.ts
new file mode 100644
index 0000000..e69de29

スカッシュをもとに戻したい場合

スカッシュを間違えてしまった時などは git reset --hard コマンドでリベース前に戻すことが可能です。方法は下記を参考にしてみてください。

おわりに

Git で複数のコミットをまとめる(スカッシュする)方法を確認してみました。

開発をする中で本来まとめたかったコミットが分かれてしまうことはあるあるだと思いまます。そのような場合はぜひスカッシュを試してみてください。

参考

以上