1. ホーム
  2. Git

【Git】git rebase -i の使い方|コミットをまとめる・書き換える方法

Share

作業を進めていると、「とりあえずコミット」「typo修正」「さっきの続き」といった細かいコミットが並んでしまうことがあります。そのままプルリクエストを出すと、レビューする人はノイズの多い履歴を追うことになります。こうしたときに使うのが git rebase -i(インタラクティブリベース)です。この記事では、コミットをまとめる・メッセージを書き直す・不要なコミットを消すといった操作を、実際のターミナルの表示とあわせて解説します。あわせて、途中で止まったときの戻し方と、やってはいけないケースも整理します。

git rebase -i で何ができるのか

git rebase は本来、あるブランチのコミットを別の地点に「付け替える」コマンドです。これに -i(--interactive)を付けると、付け替える途中でエディタが開き、どのコミットをどう扱うかを1行ずつ指示できるようになります。付け替え先を変えずに自分のコミットだけを対象にすれば、実質「直近の履歴を編集するツール」として使えます。

具体的には、複数のコミットを1つにまとめる、コミットメッセージを書き直す、コミットの順番を入れ替える、いらないコミットを丸ごと消す、過去のコミットの中身そのものを直す、といったことができます。git commit --amend が「直前の1つだけ」を直せるのに対して、git rebase -i はもっと前のコミットまでさかのぼって手を入れられる、と考えると分かりやすいです。

ただし、ここで行う操作はすべて新しいコミットを作り直す処理です。見た目は「編集」ですが、実際にはコミットのハッシュ(ID)が変わります。この性質が、後半で説明する「push 済みのコミットに使ってはいけない」理由につながります。

基本の使い方とtodoリストの読み方

起点となるコミットを指定して実行します。直近3つのコミットを対象にするなら、その1つ前を指す HEAD~3 を渡します。

ターミナル
# 直近3コミットを編集対象にする
git rebase -i HEAD~3

ここで指定する HEAD~3 は「3つ前のコミット」であり、そのコミット自体は編集対象に含まれません。編集できるのはそれより後の3つです。数え方に迷ったら、先に git log --oneline で並びを確認し、直したいコミットの1つ前のハッシュを直接渡す方法が確実です。

ターミナル
git log --oneline
# 9f1c2ab ログ出力を追加
# 7ad4e10 typo修正
# c3b9d55 ユーザー登録フォームを追加
# 1e0f8c4 初期設定

# c3b9d55 以降の3つを編集したい場合
git rebase -i 1e0f8c4

コマンドを実行するとエディタが開き、次のような「todoリスト」が表示されます。これがリベースの指示書です。

git-rebase-todo
pick c3b9d55 ユーザー登録フォームを追加
pick 7ad4e10 typo修正
pick 9f1c2ab ログ出力を追加

# Rebase 1e0f8c4..9f1c2ab onto 1e0f8c4 (3 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 <commit> = like "squash", but discard this commit's log message
# d, drop <commit> = remove commit

注意したいのは、git log と並び順が逆で、上が古いコミット、下が新しいコミットだという点です。そして操作は、各行の先頭にある pick を別のコマンドに書き換えることで指示します。行を消せばそのコミットは消え、行を入れ替えれば順番が変わります。# から始まる行はコメントなので、書き換える必要はありません。

書き換えたら保存してエディタを閉じます。するとGitが上から順に、書かれたとおりに履歴を組み立て直します。

todoリストで使えるコマンド

よく使うものは次のとおりです。いずれも先頭1文字の省略形が使えます。

コマンド意味
pick(p)そのまま残す。初期状態はすべてこれ
reword(r)コミットの内容はそのままに、メッセージだけ書き直す
edit(e)そのコミットの直後で作業を止め、内容を修正できるようにする
squash(s)1つ上のコミットに統合する。メッセージは両方をつなげて編集できる
fixup(f)squash と同じく統合するが、この行のメッセージは捨てる
drop(d)そのコミットを履歴から取り除く。行ごと削除しても同じ
break(b)その位置で一旦停止する。途中で状態を確認したいときに使う
exec(x)続けて書いたコマンドを実行する。各コミットでテストを流すときなど

複数のコミットを1つにまとめる

もっとも使う場面が、細かいコミットの統合です。先ほどの例で「typo修正」と「ログ出力を追加」を「ユーザー登録フォームを追加」に吸収させたいなら、2行目以降を squash に書き換えます。

git-rebase-todo
pick   c3b9d55 ユーザー登録フォームを追加
squash 7ad4e10 typo修正
squash 9f1c2ab ログ出力を追加

squash は「1つ上の行のコミットに合流する」という意味です。そのため先頭行を squash にすることはできません(合流先がないためエラーになります)。まとめ先にしたいコミットを一番上に置くのがポイントです。

保存して閉じると、今度はコミットメッセージを編集する画面が開きます。3つ分のメッセージが並んでいるので、不要な行を消して最終的なメッセージを整えます。

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

ユーザー登録フォームを追加

# This is the commit message #2:

typo修正

# This is the commit message #3:

ログ出力を追加

保存して閉じれば完了です。git log --oneline を見ると、3つあったコミットが1つになっているはずです。

squash と fixup の違い

この2つは「1つ上のコミットに統合する」という点では同じで、違うのはメッセージの扱いだけです。squash は統合対象のメッセージを残してエディタで編集させるのに対し、fixup はそのコミットのメッセージを黙って捨て、合流先のメッセージをそのまま使います。

「typo修正」「レビュー指摘対応」のように、履歴に残す価値のないメッセージを吸収させたいときは fixup が向いています。メッセージ編集画面も開かないので、その分手数が減ります。逆に、複数のコミットの説明を合体させて意味のあるメッセージを作りたいときは squash を使います。

git commit –fixup と –autosquash で自動化する

todoリストを毎回手で書き換えるのは面倒ですし、行を間違えると意図しないコミットに合流してしまいます。そこで、修正コミットを作る時点で「どのコミットの修正なのか」を記録しておく方法があります。

ターミナル
# 修正内容をステージする
git add src/form.js

# c3b9d55 への修正であることを記録してコミットする
git commit --fixup c3b9d55

# todoリストが自動で並べ替えられた状態でリベースを開始する
git rebase -i --autosquash 1e0f8c4

git commit --fixup は fixup! ユーザー登録フォームを追加 という決まった形式のメッセージでコミットを作ります。--autosquash を付けてリベースすると、Gitがこの目印を読み取り、対象コミットの直後にその行を移動したうえで fixup に設定した状態でtodoリストを開いてくれます。あとは内容を確認して保存するだけです。

メッセージも一緒に編集したい場合は git commit --squash c3b9d55 を使います。毎回 --autosquash を書くのが手間なら、設定で常に有効にできます。

ターミナル
git config --global rebase.autosquash true

メッセージや中身を書き換える

コミットの内容は正しいけれどメッセージだけ直したい、という場合は reword です。該当行を書き換えて保存すると、その行のメッセージだけを編集するエディタが順番に開きます。複数行を同時に reword にしても構いません。

一方、過去のコミットのファイルの中身そのものを直したいときは edit を使います。edit を指定したコミットを適用した直後でリベースが一時停止するので、そこで普通に編集して git commit --amend し、作業を再開します。

ターミナル
# edit を指定した位置で停止した状態
# Stopped at c3b9d55...  ユーザー登録フォームを追加

# ファイルを修正してステージする
git add src/form.js

# 停止中のコミットに修正を取り込む(メッセージはそのままでよければ --no-edit)
git commit --amend --no-edit

# 続きを実行する
git rebase --continue

なお、コミットの順番を入れ替えたいときは、todoリストの行そのものを上下に移動させます。ただし内容に依存関係がある場合(後のコミットが前のコミットの変更を前提にしている場合)は、入れ替えた時点でコンフリクトになります。

途中で止まったときの3つの操作

リベースは edit の指定やコンフリクトによって途中で停止します。このとき使う操作は3つだけです。

コマンド動作
git rebase --continue解決したものとして次のコミットに進む
git rebase --abortリベースを中止し、開始前の状態に完全に戻す
git rebase --skip今処理中のコミットを捨てて次に進む

迷ったら --abort です。リベースを始める前の状態にそのまま戻るので、落ち着いてやり直せます。逆に --skip はそのコミットの変更を丸ごと破棄する操作なので、内容を理解していないときには使わないでください。

コンフリクトが起きたときの流れ

コミットを並べ替えたり、離れた地点に付け替えたりすると、同じ箇所への変更がぶつかってコンフリクトになります。そのときは次のようなメッセージが出て停止します。

ターミナル
CONFLICT (content): Merge conflict in src/form.js
error: could not apply 7ad4e10... typo修正
Resolve all conflicts manually, mark them as resolved with
"git add/rm <pathspec>", then run "git rebase --continue".

やることは、ファイルを開いて <<<<<<< と >>>>>>> で囲まれた箇所を正しい内容に直し、git add でステージし、git rebase --continue を実行する、の3ステップです。ここで git commit を打つ必要はありません。--continue が自動でコミットを作り直します。

ターミナル
# どのファイルが衝突しているか確認する
git status

# 修正後にステージして続行
git add src/form.js
git rebase --continue

対象のコミットが複数あると、コンフリクトはコミットごとに繰り返し発生します。1回解決して終わりとは限らないので、Successfully rebased and updated refs/heads/... と表示されるまでが一区切りだと考えてください。

push済みのコミットをリベースしてはいけない理由

ここがインタラクティブリベースでもっとも重要な注意点です。冒頭で触れたとおり、リベースはコミットを編集するのではなく作り直します。中身が1バイトも変わっていなくても、親コミットが変われば新しいハッシュのコミットになります。つまり、リベース後の履歴は、元の履歴とは別物です。

自分のローカルにしかないコミットなら、これは何の問題も起こしません。困るのは、すでに git push してリモートに存在するコミットを書き換えた場合です。リモートには古いコミットが、手元には新しいコミットが残り、履歴が分岐した状態になります。この状態で普通に git push しても拒否されるため、--force で上書きすることになります。

そして、そのブランチをすでに取得している別のメンバーがいると被害が出ます。相手の手元には消えたはずの古いコミットが残っているため、次に git pull したときに古い履歴と新しい履歴がマージされ、同じ変更が二重に入った履歴ができあがります。誰かの作業が巻き戻る事故もここで起きます。原則として、他人と共有しているブランチのコミットはリベースしないと覚えてください。

自分だけのブランチなら –force-with-lease で push する

一方で、レビュー前の自分専用の作業ブランチであれば、履歴を整理してから push し直すのはよくある運用です。その場合も --force ではなく --force-with-lease を使ってください。

ターミナル
# 自分が最後に確認した状態から変わっていなければ上書きする
git push --force-with-lease origin feature/signup

--force はリモートの状態を一切確認せず、無条件に自分の履歴で上書きします。もし自分が最後に取得したあとに誰かがそのブランチへ push していたら、その変更は消えます。--force-with-lease は「リモートが自分の知っている状態のままであること」を条件にするため、間に誰かの push が入っていた場合は失敗して教えてくれます。共有ブランチでの事故を防ぐ安全装置として、強制 push はこちらを既定にしておくのが安心です。

リベースがうまく進まないとき

エディタが開いたまま先に進めない

Gitの初期設定では vi(Vim)でtodoリストが開きます。使い慣れていないと保存も終了もできず、リベースが始まりません。Vimの場合は i で編集モードに入り、編集後に Esc を押してから :wq で保存終了、やめたいときは :q! です。

そもそも使うエディタを変えてしまうほうが確実です。VS Codeを使うなら次のように設定します。--wait は「エディタのタブを閉じるまでGitを待たせる」ためのオプションで、これがないとtodoリストを編集する前にリベースが進んでしまいます。

ターミナル
git config --global core.editor "code --wait"

「You have unstaged changes」と言われて始まらない

リベースは作業ツリーがきれいな状態でないと開始できません。編集途中のファイルが残っていると error: cannot rebase: You have unstaged changes. で止まります。先にコミットしてしまうか、git stash で退避してからリベースし、終わってから git stash pop で戻します。

操作を間違えて履歴を壊してしまった

リベースの最中であれば git rebase --abort で元に戻せます。問題は、完了したあとで「まとめるコミットを間違えた」「必要なコミットを消してしまった」と気づいたときです。この場合は git reflog を使います。

ターミナル
git reflog
# 4b8d1f0 HEAD@{0}: rebase (finish): returning to refs/heads/feature/signup
# 4b8d1f0 HEAD@{1}: rebase (squash): ユーザー登録フォームを追加
# 9f1c2ab HEAD@{4}: commit: ログ出力を追加   ← リベース前の状態

# リベース前の位置にブランチを戻す
git reset --hard HEAD@{4}

git reflog は HEAD が移動した履歴をすべて記録しているため、リベースで「消えた」ように見えるコミットもここから辿れます。rebase (start) より前の行が開始前の状態です。ただし git reset --hard は現在の変更を破棄するので、実行前に作業ツリーがきれいかどうかを必ず確認してください。

まとめ

git rebase -i <起点> を実行すると、その起点より後のコミットを1行ずつ指示できるtodoリストが開きます。pick を書き換えるだけで、メッセージの修正(reword)、内容の修正(edit)、統合(squash / fixup)、削除(drop)、並べ替えができます。squash と fixup の違いはメッセージを残すかどうかだけで、typo修正のようなコミットは git commit --fixup と --autosquash を組み合わせると手作業なしで畳めます。

途中で止まったら --continue で進め、迷ったら --abort で開始前に戻す。これだけ覚えておけば行き詰まりません。そして、リベースはコミットを作り直す操作なので、すでに共有されているコミットには使わないことが大原則です。自分の作業ブランチを整理して push し直すときは、--force ではなく --force-with-lease を使ってください。

参考ページ