9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

git rebaseでコミットをまとめたり分割する

Last updated at Posted at 2022-06-16

Gitを使っていると、1つの機能追加が複数のコミットに分かれていたり、1つのコミットに関係ない修正が2つ以上含まれてしまっていたりすることが良くあると思います。

個人的な理想はリリース用ブランチのコミット (のタイトル) を見るだけで、プロダクトにいつ・どんな修正が加わったか分かるようにしたいのですが、そのためにはコミットの粒度を適切に分割・統合する必要があります。

git rebaseを使えば、連続していない複数のコミットを1つにまとめたり、1つのコミットを2つ以上に分割することが出来ます。

普段コミットのタイトルや粒度を意識してなかった人も、ぜひここで紹介するテクニックを使って、レビュー前にコミットを分かりやすく整理してみて欲しいと思います。

またこのテクニックを覚えると、余計なコミットを作るのを避けてギリギリまでgit commit出来なかった人も、一時保存のために気軽にgit commitが出来るようになると思います。

rebase練習用リポジトリとコミットの作成

今回は実際にrebaseでのコミットの分割・統合を経験してもらえるよう、練習用のリポジトリとコミットを作って、これを操作する前提で解説していきます。

練習用のコミットして、下記のコミットを作っていきます。

  • commit1: A機能用ソースファイル追加
  • commit2: A機能の説明をREADMEに追記
  • commit3: READMEにタイプミス発覚→修正
  • commit4: B機能用ソースファイル追加
  • commit5: B機能の説明をREADMEに追記
  • commit6: B機能に仕様変更発生→ソース修正
  • commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
  • commit8: A機能でバグ発覚→A機能のソース修正
#練習用リポジトリ作成
mkdir git-rebase
cd git-rebase/
git init

#first commit作成
touch README.md
git add README.md
git commit -m "first commit"

#開発用ブランチ作成 (メインブランチからforkした機能開発用のブランチ)
git switch -c develop
git branch
* develop         #←現在developブランチで作業していることを確認 (*がアクティブなブランチ)
  master

#commit1作成
echo "feat A" > featA.src
git add featA.src
git commit -m "commit1: A機能用ソースファイル追加"

#commit2作成
echo "fead A's README" >> README.md
git add README.md
git commit -m "commit2: A機能の説明をREADMEに追記"

#commit3作成
sed -i "s/fead/feat/g" README.md
git add README.md
git commit -m "commit3: READMEにタイプミス発覚→修正"

#commit4作成
echo "feat B" > featB.src
git add featB.src
git commit -m "commit4: B機能用ソースファイル追加"

#commit5作成
echo "feat B's README" >> README.md
git add README.md
git commit -m "commit5: B機能の説明をREADMEに追記"

#commit6作成
echo "feat B modified" > featB.src
git add featB.src
git commit -m "commit6: B機能に仕様変更発生→ソース修正"

#commit7作成
git revert HEAD
##エディタが開くので1行目を下記のように修正
commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
##:wq or ZZで上書き保存してエディタを抜ける

#commit8作成
echo "feat A modified" > featA.src
git add featA.src
git commit -m "commit8: A機能でバグ発覚→A機能のソース修正"

#コミットグラフを確認
git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* b3efbfe (HEAD -> develop) commit8: A機能でバグ発覚→A機能のソース修正
* 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
* b522341 commit6: B機能に仕様変更発生→ソース修正
* 0ce0d64 commit5: B機能の説明をREADMEに追記
* 139de71 commit4: B機能用ソースファイル追加
* 25d6a63 commit3: READMEにタイプミス発覚→修正
* d21ffa1 commit2: A機能の説明をREADMEに追記
* d0290fa commit1: A機能用ソースファイル追加
* b7b9572 (master) first commit

最終的にリポジトリのファイル内容は下記の状態になります。
(#行が出力内容です。コマンドを区別するために頭に# を追加しています)

ls | sed "s/^/# /g"
# featA.src
# featB.src
# README.md

cat featA.src | sed "s/^/# /g"
# feat A modified

cat featB.src | sed "s/^/# /g"
# feat B

cat README.md | sed "s/^/# /g"
# feat A's README
# feat B's README

rebase作業の前準備

今回はコミット履歴を改変するテクニックを紹介しますので、失敗すると元に戻せなくなるおそれがあります。
rebaseの途中であれば、git rebase --abortでなかったことに出来ますが、rebaseを完了させてしまうと後戻り出来ません。

そこで、現在のブランチはそのままに、rebase作業用のブランチを作ってそこで作業するのが安全です。
現在のブランチを残しておけば、作業後に元のブランチとrebase後のブランチをdiffして、差分が無いことを確認することも出来ます。

git branch
* develop         #←現在のブランチを確認 (*がアクティブなブランチ)
  master

#rebase作業用にworkブランチを作成してスイッチ
git switch -c work

git branch
  develop
  master
* work            #←現在workブランチで作業していることを確認 (*がアクティブなブランチ)

git diff develop work
#出力なし。まだ何もしてないので差分はない
#これからコミットをこねくり回すが、最終的にこのコマンドで差分が出ないことを確認する

複数のコミットをまとめたり順番を変えたりする

バラバラになっているコミットを適切な単位にまとめましょう。

整理前のコミットがこちら。

git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* b3efbfe (HEAD -> work, develop) commit8: A機能でバグ発覚→A機能のソース修正
* 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
* b522341 commit6: B機能に仕様変更発生→ソース修正
* 0ce0d64 commit5: B機能の説明をREADMEに追記
* 139de71 commit4: B機能用ソースファイル追加
* 25d6a63 commit3: READMEにタイプミス発覚→修正
* d21ffa1 commit2: A機能の説明をREADMEに追記
* d0290fa commit1: A機能用ソースファイル追加
* b7b9572 (master) first commit

これを下記のように整理します。

  • commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
    • A機能で1つにまとめる
    • タイプミスの修正やリリース前のバグ修正の履歴を公開する必要はないので削除する
  • commit4+5: B機能とB機能の説明を含むREADME
    • B機能で1つにまとめる
    • 「リリース前に一度追加したけど削除した機能」の履歴を公開する必要はないのでcommit6,7は削除する

ここからrebaseに入りますが、途中で操作を間違えた場合はgit rebase --abortを実行することでここまで巻き戻ることが出来ます。
abort後はここからやり直してください。

git branch
  develop
  master
* work            #←現在workブランチで作業していることを確認 (*がアクティブなブランチ)

#インタラクティブモードのrabaseを起動
##first commitから現在までのコミットを整理したいので、rebase先はmasterを指定する。first commitのコミットIDでも可
##仮にcommit4から現在までを整理したい場合はcommit3のコミットIDをrabase先に指定する
git rebase -i master
 
#viエディタが起動し、下記のように表示される
pick d0290fa commit1: A機能用ソースファイル追加
pick d21ffa1 commit2: A機能の説明をREADMEに追記
pick 25d6a63 commit3: READMEにタイプミス発覚→修正
pick 139de71 commit4: B機能用ソースファイル追加
pick 0ce0d64 commit5: B機能の説明をREADMEに追記
pick b522341 commit6: B機能に仕様変更発生→ソース修正
pick 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
pick b3efbfe commit8: A機能でバグ発覚→A機能のソース修正
 
#これを下記のように書き換える
## コミットの順番を変える
## 上と統合したいコミットのpickをsquashに書き換える
## 不要なコミットの行を削除する
pick commit1 A機能用ソースファイル追加
squash commit2 A機能の説明をREADMEに追記
squash commit3 タイプミス発覚→ソース修正
squash commit8 A機能バグ修正
pick commit4 B機能用ソースファイル追加
squash commit5 B機能の説明をREADMEに追記
 
#:wqまたはZZでviエディタを終了する

コミット整理用のviエディタを閉じると、commit1+2+3+8のコミットメッセージを編集するviエディタが起動します。
内容は下記の通りです。

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

commit1: A機能用ソースファイル追加

# This is the commit message #2:

commit2: A機能の説明をREADMEに追記

# This is the commit message #3:

commit3: READMEにタイプミス発覚→修正

# This is the commit message #4:

commit8: A機能でバグ発覚→A機能のソース修正

コミットメッセージなので内容は何でも良いのですが、今回は下記のように書き換えます。
(実際の開発現場では他の人にも分かりやすいコミットメッセージを書くように心掛けてください)

commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME

commit1: A機能用ソースファイル追加
commit2: A機能の説明をREADMEに追記
commit3: READMEにタイプミス発覚→修正
commit8: A機能でバグ発覚→A機能のソース修正

上記の通り編集したら:wqorZZで上書き保存してviエディタを終了してください。
すると次にcommit4+5のコミットメッセージを編集するviエディタが起動します。
内容は下記の通りです。

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

commit4: B機能用ソースファイル追加

# This is the commit message #2:

commit5: B機能の説明をREADMEに追記

こちらも下記ように書き換えてください。

ommit4+5: B機能とB機能の説明を含むREADME

commit4: B機能用ソースファイル追加
commit5: B機能の説明をREADMEに追記

編集を終えたら:wqorZZで上書き保存してviエディタを終了してください。
その後下記のメッセージが表示されたらrabase完了です。

[detached HEAD 014d976] commit4+5: B機能とB機能の説明を含むREADME
 Date: Wed Jun 8 07:25:27 2022 +0000
 2 files changed, 2 insertions(+)
 create mode 100644 featB.src
Successfully rebased and updated refs/heads/work.

コミットグラフを確認すると、整理前に8個だったコミットが2個にまとまっているのが分かります。

git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 014d976 (HEAD -> work) commit4+5: B機能とB機能の説明を含むREADME
* 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
| * b3efbfe (develop) commit8: A機能でバグ発覚→A機能のソース修正
| * 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
| * b522341 commit6: B機能に仕様変更発生→ソース修正
| * 0ce0d64 commit5: B機能の説明をREADMEに追記
| * 139de71 commit4: B機能用ソースファイル追加
| * 25d6a63 commit3: READMEにタイプミス発覚→修正
| * d21ffa1 commit2: A機能の説明をREADMEに追記
| * d0290fa commit1: A機能用ソースファイル追加
|/
* b7b9572 (master) first commit

git diff develop work
#出力なし
#rabase前のdevelopブランチとrebase後のworkブランチを比較して差分が無いことを確認する

1つのコミットを2つ以上に分割する

今度は、さっきまとめたコミットを、機能追加とREADME追加で別のコミットに分離してみます。

分割前の状態がこちら。

git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 014d976 (HEAD -> work) commit4+5: B機能とB機能の説明を含むREADME
* 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
* b7b9572 (master) first commit

このうち、commit1+2+3+8を分割して下記のようにしてみます。

  • commit1+8: A機能(バグ修正済み)追加
  • commit2+3: A機能のREADME追加
  • commit4+5: B機能とB機能の説明を含むREADME

まずは今回も安全のために作業用ブランチを作成します。

git branch
  develop
  master
* work            #←現在workブランチで作業していることを確認 (*がアクティブなブランチ)

#分割作業用に新しいブランチを作成する
git switch -c work2
git branch
  develop
  master
  work
* work2

そしてrebaseしていきます。
rebase中に操作を間違えた場合はgit rebase --abortを実行することでここまで巻き戻ることが出来ます。
abort後はここからやり直してください。

#インタラクティブモードのrabaseを起動
git rebase -i master
 
#viエディタが起動し、下記のように表示される
pick 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
pick 014d976 commit4+5: B機能とB機能の説明を含むREADME
 
#これを下記のように書き換える
##分割したいコミットのpickをeditに書き換える
edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
pick 014d976 commit4+5: B機能とB機能の説明を含むREADME
 
#:wqまたはZZでviエディタを終了すると下記のメッセージが表示される
Stopped at 8be3cfc...  commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
You can amend the commit now, with
  git commit --amend
Once you are satisfied with your changes, run
  git rebase --continue

ここからコミットを分離していくのですが、厳密には「コミットを分離する」という機能はrabaseにはありません。
ここからはインタラクティブrabaseの状態で、任意の粒度のコミットを手動で作り直していく作業になります。

まず、現在の状態はこのようになっています。

git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME  #←このコミットを適用した状態
(後略)

git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 8be3cfc (HEAD) commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
* b7b9572 (master) first commit

rebaseするときにcommit1+2+3+8をeditにしておいたのでここでrebaseが一時停止しています。
commit1+2+3+8がすでにコミットされていますが、今回はこのコミットを分割したいので、このコミットはresetで無かったことにします。

#git resetで強制的に一つ前の状態まで巻き戻す
##--hardオプション付きでgit resetすると、ファイル内容も含めて指定コミットの状態に戻ります
##HEADは現在位置、^は一つ前を意味します。HEAD^で現在の一つ前という意味になります
git reset --hard HEAD^
HEAD is now at b7b9572 first commit

#HEADが一つ前に戻っていることを確認
git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* b7b9572 (HEAD, master) first commit
git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 014d976 (work2, work) commit4+5: B機能とB機能の説明を含むREADME
* 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
| * b3efbfe (develop) commit8: A機能でバグ発覚→A機能のソース修正
| * 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
| * b522341 commit6: B機能に仕様変更発生→ソース修正
| * 0ce0d64 commit5: B機能の説明をREADMEに追記
| * 139de71 commit4: B機能用ソースファイル追加
| * 25d6a63 commit3: READMEにタイプミス発覚→修正
| * d21ffa1 commit2: A機能の説明をREADMEに追記
| * d0290fa commit1: A機能用ソースファイル追加
|/
* b7b9572 (HEAD, master) first commit

#git statusでもクリーンなことを確認
git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
(中略)
nothing to commit, working tree clean         #←このメッセージが出ることを確認

これでcommit1+2+3+8を無かったことにしたので、まずは「commit1+8: A機能(バグ修正済み)追加」というコミットを作っていきます。

#commit1+2+3+8 (コミットID: 8be3cfc) からfeatA.srcを復元してコミットする
git restore --source 8be3cfc featA.src
git add featA.src
git commit -m "commit1+8: A機能(バグ修正済み)追加"
[detached HEAD 3e4a18d] commit1+8: A機能(バグ修正済み)追加
 1 file changed, 1 insertion(+)
 create mode 100644 featA.src

#コミットグラフを確認
git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 2ca3823 (HEAD) commit1+8: A機能(バグ修正済み)追加
* b7b9572 (master) first commit

#現状確認
git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME  #←まだここのedit中
(後略)

これでcommit1+8のコミットが出来上がったので、次は「commit2+3: A機能のREADME追加」を作成します。

#commit1+2+3+8 (コミットID: 8be3cfc) からREADME.mdを復元してコミットする
git restore --source 8be3cfc README.md
git add README.md
git commit -m "commit2+3: A機能のREADME追加"
[detached HEAD 5875d0f] commit2+3: A機能のREADME追加
 1 file changed, 1 insertion(+)

#コミットグラフを確認
git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* d4a6299 (HEAD) commit2+3: A機能のREADME追加
* 2ca3823 commit1+8: A機能(バグ修正済み)追加
* b7b9572 (master) first commit

#commit1+2+3+8 (コミットID: 8be3cfc) と差分が無いことを確認
git diff 8be3cfc
#出力なし

これでcommit1+2+3+8をcommit1+8とcommit2+3に分割出来たので、rabaseのステータスをcommit1+2+3+8の編集状態から次へと進めます。

#現状確認
git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME  #←まだここのedit中
(後略)

#rebaseのステップを1つ進める
git rebase --continue
Successfully rebased and updated refs/heads/work2.

これでrebaseが完了したので状態を確認します。

git status
On branch work2
nothing to commit, working tree clean

git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* a152d3d (HEAD -> work2) commit4+5: B機能とB機能の説明を含むREADME
* d4a6299 commit2+3: A機能のREADME追加
* 2ca3823 commit1+8: A機能(バグ修正済み)追加
* b7b9572 (master) first commit

git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* a152d3d (HEAD -> work2) commit4+5: B機能とB機能の説明を含むREADME
* d4a6299 commit2+3: A機能のREADME追加
* 2ca3823 commit1+8: A機能(バグ修正済み)追加
| * 014d976 (work) commit4+5: B機能とB機能の説明を含むREADME
| * 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
|/
| * b3efbfe (develop) commit8: A機能でバグ発覚→A機能のソース修正
| * 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
| * b522341 commit6: B機能に仕様変更発生→ソース修正
| * 0ce0d64 commit5: B機能の説明をREADMEに追記
| * 139de71 commit4: B機能用ソースファイル追加
| * 25d6a63 commit3: READMEにタイプミス発覚→修正
| * d21ffa1 commit2: A機能の説明をREADMEに追記
| * d0290fa commit1: A機能用ソースファイル追加
|/
* b7b9572 (master) first commit

git diff work work2
#出力なし。コミット分割前と比べて差分なし

git diff develop work2
#出力なし。コミット結合・分割前と比べても差分なし

commit1+8とcommit2+3はrebase中に手動で作成したコミットで、commit4+5はrebaseが自動的に作成したコミットです。
work2ブランチをmasterまでresetして手動で1つ1つ任意のコミットを作っていっても同じことが出来ますが、コミットの数が多いときは手動編集したいコミット以外はrebaseにお任せ出来るので楽です。

9
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?