本文へ移動
購読
GUIDE#devtools更新 2026.09.24

git rebaseの使い方とreset・revertによる取り消し:履歴を壊さない判断基準

git rebaseで作業ブランチを最新のmainに追従させる手順と、reset・revertでの取り消し方。rebase -i、コンフリクト対処、reflogでの復旧、push済みかで決める判断表まで。

level
中級
time
約25分
stack
Git 2.55(Ubuntu 24.04標準の2.43でも可)
author
@ryoma_t
// 前提条件
git add・commit・push・ブランチの作成ができること / 壊してもよい練習用の環境があること
git rebase mainでfeatureブランチを付け替え、前後のログのグラフとreflogを確認したターミナル
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

git rebaseとは、ブランチ上のコミットを別のコミット(新しい土台)の上に1つずつ作り直し、履歴を一直線に並べ替えるコマンドです。作り直すのでコミットのハッシュが変わり、どこまでなら使ってよいか、失敗したらどう戻すかもこの性質から決まります。取り消しに使うresetとrevertもあわせて、「そのコミットをもう誰かと共有したか」を軸に使い分けを整理します。

コマンドと出力は、執筆時点の最新版Git 2.55.0で実行したものです(Ubuntu 24.04標準のGit 2.43.0との表示の違いも補足します)。同じ状態を手元で作れる練習用リポジトリのスクリプトを載せているので、壊してもよい環境で試してから実務のリポジトリに使ってください。

この記事でわかること

  • rebaseとmergeの違いと、コミットを「作り直す」仕組み
  • git rebaseとgit rebase -i(squash・fixup・autosquash)の使い方
  • コンフリクトの直し方と、--continue・--skip・--abortの違い
  • reflogとORIG_HEADでrebaseやresetを取り消す方法
  • resetの3モードとrevertの違い、push済みかどうかで決める取り消し方の判断表

git rebaseとは:mergeとの違いと「作り直す」仕組み

mergeは、2本の履歴を合流させるマージコミットを1つ足すだけで、既存のコミットには手を付けません。一方のrebaseは、作業ブランチのコミットから差分を1つずつ取り出し、新しい土台の上で順に適用し直します。内容が同じでも親のコミットが変わるため、ハッシュはすべて新しくなります。後述の練習では、rebase前に32860dc・f62ce59・8512cd8だった3つのコミットが、rebase後は953ca35・1e4d112・d2af58cになりました。

観点mergerebase
履歴の形枝分かれとマージコミットが残る一直線になる
既存コミットのハッシュ変わらない作業ブランチ側がすべて変わる
コンフリクトの解消マージ時に1回作り直すコミットごとに起こりうる
push済みのブランチで使うとそのままpushできる強制pushが必要。他の人の手元の履歴とずれる
向く場面mainなど共有ブランチへの取り込み自分だけの作業ブランチを最新のmainに追従させる

作り直される前の古いコミットは、すぐには消えません。どのブランチからもたどれなくなったコミットでも、reflog(HEADやブランチが指したコミットの記録)には既定で30日間残ります(gc.reflogExpireUnreachableの既定値)。失敗したrebaseを元へ戻せるのはこのためで、手順は後半で扱います。

git rebaseの使い方:mainの変更を作業ブランチに取り込む

まず練習用のリポジトリを作ります。ホームディレクトリで次を実行すると、mainから分かれたfeature/loginに3コミット、mainに1コミットがある状態になります。コミットのハッシュは作成日時も含めて計算されるため、手元で実行すると本文の例とは違う値になります。

practice.sh(練習用リポジトリの作成)
cd ~
git init -b main rebase-demo && cd rebase-demo
printf 'title=Demo\nport=8080\n' > app.conf
git add . && git commit -m "初期設定"
git switch -c feature/login
printf '<form>login</form>\n' > login.html
git add . && git commit -m "ログイン画面を追加"
printf 'function validate() {}\n' > validate.js
git add . && git commit -m "入力チェックを追加"
printf '<form>Login</form>\n' > login.html
git commit -am "typo修正"
git switch main
printf 'log_level=info\n' >> app.conf
git commit -am "ログ設定を追加"
git switch feature/login
terminal
cd ~/rebase-demo
git log --oneline --graph --all
git rebase main
git log --oneline --graph --all
git reflog -5
出力(Git 2.55.0)
* 8512cd8 typo修正
* f62ce59 入力チェックを追加
* 32860dc ログイン画面を追加
| * 930aec0 ログ設定を追加
|/
* 34968a5 初期設定

Successfully rebased and updated refs/heads/feature/login.

* d2af58c typo修正
* 1e4d112 入力チェックを追加
* 953ca35 ログイン画面を追加
* 930aec0 ログ設定を追加
* 34968a5 初期設定

d2af58c HEAD@{0}: rebase (finish): returning to refs/heads/feature/login
d2af58c HEAD@{1}: rebase (pick): typo修正
1e4d112 HEAD@{2}: rebase (pick): 入力チェックを追加
953ca35 HEAD@{3}: rebase (pick): ログイン画面を追加
930aec0 HEAD@{4}: rebase (start): checkout main

実行前のfeature/loginは34968a5から枝分かれしていましたが、実行後はmainの最新930aec0の上に3コミットが一直線に並びます。reflogを見ると、mainへ移動(start)→3回のpick→元のブランチへ戻る(finish)という手順で作り直したことがわかります。

日々のgit pullも、統合方法をrebaseにできます。git pull --rebaseはfetchのあとにrebaseする省略形で、git config --global pull.rebase trueにすると既定になります。未コミットの変更があるとrebaseは始まらないため、git config --global rebase.autoStash trueも入れておくと、変更を一時退避して終わったあとに戻してくれます。マージコミットで取り込みの記録を残したいチームなら、代わりにpull.ff onlyを設定し、早送りできないときは手でmergeします。

コミットをまとめる・直す:rebase -i

git rebase -i mainは、mainから先のコミットを一覧にしたTODOリストをエディタで開き、書き換えた指示どおりに作り直します。Git 2.55では件名の前に#が付き、2.43では付きません。行の並べ替えもコミットの順序の変更になります。

git rebase -i main で開くリスト(Git 2.55.0)
pick 953ca35 # ログイン画面を追加
pick 1e4d112 # 入力チェックを追加
pick d2af58c # typo修正
書き換える語動作使いどころ
pickそのまま使う既定
rewordメッセージだけ直すコミットメッセージの誤字
editそのコミットで止まる内容を直す、1つのコミットを分割する
squash直前のコミットにまとめ、メッセージも合成する細かい途中経過を1つにまとめる
fixup直前のコミットにまとめ、自分のメッセージは捨てる「typo修正」のような後追いの修正
dropコミットを消す試しに入れたデバッグ用のコミット

後から直しを入れると分かっている場合は、--fixup付きでコミットしておくと、並べ替えを手で書かずに済みます。git commit --fixup=<直したいコミット>は件名がfixup! 元の件名のコミットを作り、git rebase -i --autosquash mainがそれを対象のすぐ下へfixupとして並べます。

terminal
$ git commit -a --fixup=HEAD~1
[feature/login 2220439] fixup! 入力チェックを追加
$ git rebase -i --autosquash main
--autosquash で開くリスト(編集せずに保存して閉じる)
pick 953ca35 # ログイン画面を追加
pick 1e4d112 # 入力チェックを追加
fixup 2220439 # fixup! 入力チェックを追加
pick d2af58c # typo修正
terminal
$ git log --oneline main..
230a70d typo修正
920f502 入力チェックを追加
953ca35 ログイン画面を追加

まとめた結果、「入力チェックを追加」は1e4d112から920f502に、その上の「typo修正」も230a70dに作り直されました。一番下の953ca35は手を付けていないのでハッシュが変わりません。作り直しの影響が及ぶのは、書き換えたコミットとそれより上のすべて、と覚えておくと範囲を見積もれます。

コンフリクトが起きたとき:--continue・--skip・--abort

mainと作業ブランチが同じ行を変えていると、その行を含むコミットを作り直すところでrebaseが止まります。練習では、mainでport=8081、作業ブランチでport=9090に変えたあとにrebaseしました。

出力(git rebase main)
Auto-merging app.conf
CONFLICT (content): Merge conflict in app.conf
error: could not apply ca2d0a3... ポートを9090に変更
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply ca2d0a3... # ポートを9090に変更
app.conf(止まった時点)
title=Demo
<<<<<<< HEAD
port=8081
log_level=info
=======
port=9090
>>>>>>> ca2d0a3 (ポートを9090に変更)

作り直しの途中では、HEAD側が「新しい土台」、つまりmain側の内容です。mergeのときと表示の向きが逆に感じられるので注意します。正しい内容に書き直して<<<<<<<などの印を消したら、git add app.confで解決済みにし、git rebase --continueで次のコミットへ進みます。

コマンド何が起きるか
git rebase --continuegit addした解決内容でそのコミットを作り、次へ進む
git rebase --skip止まっているコミットの変更を捨てて次へ進む(そのコミットは消える)
git rebase --abortrebaseを始める前の状態にブランチと作業ツリーを戻す

--skipは「このコミットを飛ばす」ではなく「このコミットの変更を捨てる」操作です。解消に疲れて打つと、作業の一部が静かに消えます。迷ったら--abortで最初からやり直すほうが安全です。同じコンフリクトを何度も解く羽目になる場合は、git config --global rerere.enabled trueで解消の仕方を記録させると、次回から自動で適用されます。

ORIG_HEADとreflogでrebaseを取り消す

完了した直後なら、ORIG_HEADにrebase前の先端が入っています。git reset --hard ORIG_HEADで戻せます。ただしORIG_HEADはresetやmergeを実行するたびに上書きされるので、別の操作を挟んだあとはブランチのreflogから戻したい地点を探します。

terminal
$ git reset --hard ORIG_HEAD
HEAD is now at 8512cd8 typo修正
$ git reflog show feature/login -3
d2af58c feature/login@{0}: rebase (finish): refs/heads/feature/login onto 930aec09a9a7b3995b90d4f8ba84c2c209b8a463
8512cd8 feature/login@{1}: commit: typo修正
f62ce59 feature/login@{2}: commit: 入力チェックを追加

ブランチのreflogではfeature/login@{1}がrebase直前の状態なので、git reset --hard feature/login@{1}でも同じ場所へ戻れます。HEADのreflog(git reflog)はブランチの切り替えも記録するため、行が多くて探しにくいときはブランチ名を指定するほうが早く見つかります。

resetの3つのモード:--soft・--mixed・--hard

git resetは、ブランチの先端(HEAD)を指定したコミットへ動かすコマンドで、インデックス(git addした内容)と作業ツリー(実際のファイル)をどこまで一緒に戻すかをモードで選びます。

モードHEADインデックス作業ツリー使う場面
--soft戻すそのままそのまま直前のコミットをやり直す(内容を足して再コミット)
--mixed(既定)戻す戻すそのままコミットとgit addを取り消し、変更はファイルに残す
--hard戻す戻す戻す変更ごと捨てる
terminal
$ printf 'memo\n' > note.txt
$ git add note.txt && git commit -qm "メモを追加"
$ git reset --soft HEAD~1 && git status -s
A  note.txt
$ git reset --mixed HEAD && git status -s
?? note.txt

--softのあとはnote.txtがaddされた状態(A)で残り、--mixedでaddも外れて未追跡(??)に戻りました。--hardで同じコミットを取り消すとnote.txtごと消えます。ファイルの変更だけ、addだけを取り消したいなら、コミットを動かさないgit restore ファイル・git restore --staged ファイルのほうが意図がはっきりします。

git reset --hardで捨てた「コミットしていない変更」はreflogに残らないため、基本的に戻せません。一方で、一度もaddしていない新規ファイル(未追跡ファイル)は--hardでも消えずに残ります。検証でもuntracked.txtは残りました。未追跡ファイルまで消したいときは、git clean -nで対象を確かめてからgit clean -fdを実行します。

公開済みのコミットはgit revertで取り消す

git revertは、指定したコミットの変更をちょうど打ち消す新しいコミットを足します。既存の履歴を書き換えないので、mainのように他の人がpullしているブランチでも、普通のpushで取り消しを共有できます。マージコミットを取り消すときは、どちらの親を「本流」とみなすかを-mで指定する必要があり、付けないと止まります。

terminal
$ git switch main
$ git merge --no-ff feature/login -m "Merge branch 'feature/login'"
Merge made by the 'ort' strategy.
$ git revert HEAD
error: commit e25d8e03c18a1dd4efe10137a7e9e77e82ec53de is a merge but no -m option was given.
fatal: revert failed
$ git revert --no-edit -m 1 HEAD
[main e74e778] Revert "Merge branch 'feature/login'"
 Date: Thu Sep 24 01:03:50 2026 +0000
 2 files changed, 2 deletions(-)
 delete mode 100644 login.html
 delete mode 100644 validate.js

-m 1は「1番目の親(マージを実行したときにいたmain)を正として、2番目の親から入った変更を打ち消す」という意味です。git merge feature/loginで作ったマージコミットなら、ほぼ常に-m 1です。

マージをrevertしたあと、修正した同じブランチをもう一度マージしても、revertで消した変更は戻りません。Gitは「そのコミットはマージ済み」と判断し、再マージで入るのは前回のマージより後のコミットだけだからです。小さなリポジトリで試したところ、マージをrevertしたあとにブランチ(feat)へ1コミット足して再マージすると、CONFLICT (modify/delete): f deleted in HEAD and modified in feat.になりました(fはブランチ側で追加したファイル)。変更を戻したいときは、先にrevertコミット自体をrevertしてから再マージします。

取り消し方の判断表:push済みかどうかで決める

どのコマンドを使うかは、「取り消したいコミットを、自分以外の誰かがもう持っているか」でほぼ決まります。持っていないなら作り直してよく、持っているなら打ち消すコミットを足します。

状況使うコマンド理由・注意
直前のコミットのメッセージや内容を直したい(未push)git commit --amend または git reset --soft HEAD~1履歴を書き換えるのでpush前に限る
作業ブランチのコミットを整理したい(自分専用)git rebase -i mainpush済みなら--force-with-leaseでの再pushが必要
mainの更新を作業ブランチに取り込みたい自分専用ならgit rebase main、共有中ならgit merge main共有ブランチの書き換えは他人の手元とずれる
mainにpush済みのコミットを取り消したいgit revert <コミット>打ち消しのコミットを足すので誰の履歴も壊れない
マージ済みのブランチごと取り消したいgit revert -m 1 <マージコミット>再マージの前にrevertをrevertする
rebaseやresetを間違えたgit reset --hard ORIG_HEAD、またはreflogで探すコミットしていない変更は戻らない
addやファイルの変更だけ取り消したいgit restore --staged / git restoreコミットは動かさない

push済みの自分のブランチをrebaseしたら、そのままのpushは拒否されます。上書きするときは--forceではなく--force-with-leaseを使います。こちらは「リモートのブランチが、自分が最後に取得した状態から動いていない場合だけ上書きする」ため、同僚が同じブランチに足したコミットを知らずに消す事故を防げます。mainのような共有ブランチは、GitHubなどのブランチ保護で強制pushそのものを禁止しておきます。ブランチの運用ルール全体は小規模チームのGitブランチ戦略にまとめています。

検証時の出力例(リモートはローカルのベアリポジトリ)
$ git push
To /work/remote.git
 ! [rejected]        feature/login -> feature/login (non-fast-forward)
error: failed to push some refs to '/work/remote.git'
$ git push --force-with-lease
To /work/remote.git
 + 84707dc...221f041 feature/login -> feature/login (forced update)

つまずきポイント

症状・エラーメッセージ原因対処
! [rejected] feature/login -> feature/login (non-fast-forward)rebaseでハッシュが変わり、リモートの履歴と食い違った自分専用のブランチならgit push --force-with-lease。他の人も使っているなら、fetchしてからgit log origin/feature/loginで相手のコミットを確かめる
fatal: Need to specify how to reconcile divergent branches.手元とリモートの両方に新しいコミットがあり、git pullの統合方法(mergeかrebaseか)が設定されていないgit config --global pull.rebase true(rebaseで統合)かpull.ff only(早送りのみ)を設定する
error: commit e25d8e0… is a merge but no -m option was given.マージコミットをrevertしようとしたgit revert -m 1 <コミット>で本流の親を指定する
CONFLICT (content): Merge conflict in app.confとerror: could not apply ca2d0a3...作り直すコミットとmainが同じ行を変えたファイルを直してgit add、git rebase --continue。やり直すならgit rebase --abort

動作確認

rebaseの直後は、コミットの中身が作り直しの前後で変わっていないかをgit range-diffで確かめます。mainを土台に、rebase前(ORIG_HEAD)と後(HEAD)のコミット列を1つずつ突き合わせ、差分が同じなら=、変わっていれば!と差分を表示します。解消したコミットだけが!になっていれば、意図どおりの作り直しです。

terminal
$ git range-diff main ORIG_HEAD HEAD
1:  32860dc = 1:  953ca35 ログイン画面を追加
2:  f62ce59 = 2:  1e4d112 入力チェックを追加
3:  8512cd8 = 3:  d2af58c typo修正
$ git log --oneline --graph -4
* d2af58c typo修正
* 1e4d112 入力チェックを追加
* 953ca35 ログイン画面を追加
* 930aec0 ログ設定を追加

よくある質問

Git rebaseはいつ使うべき?

自分だけが使っている作業ブランチを最新のmainの上に載せ直したいときと、レビューに出す前にコミットを整理したいときです。mainのように他の人も使うブランチや、すでに誰かが取り込んだコミットには使いません。迷ったら「このブランチを自分以外がpullしているか」で判断し、しているならmergeにします。

Mergeとrebaseのどっちが安全ですか?

既存のコミットを書き換えないmergeのほうが安全です。コミットを作り直すrebaseは、共有済みのブランチで行うと他の人の手元の履歴と食い違います。ただしpush前の自分のブランチで行う限り、失敗してもreflogから戻せます。危ないのは「共有ブランチで」「強制pushする」組み合わせです。

Rebaseを中止するとどうなる?

途中でgit rebase --abortを実行すると、ブランチはrebaseを始める前のコミットに戻り、作業ツリーも元の状態になります。コンフリクトの解消中に編集した内容は捨てられます。完了したあとのrebaseを取り消す場合はabortではなく、git reset --hard ORIG_HEADかreflogを使います。

Gitのrebaseとpullの違いは何ですか?

pullは、fetchでリモートの変更を取得して手元のブランチに統合するコマンドで、統合の方法としてmergeかrebaseを選べます。git pull --rebaseはfetchのあとにrebaseする省略形です。一方でrebase単体はリモートと通信せず、手元のコミットを別の土台へ載せ直すだけです。

まとめ

共有済みのコミットはrevertで打ち消し、自分だけのブランチはrebaseやresetで作り直す、という線引きが判断の軸です。作り直した後はgit range-diffで中身が変わっていないかを確かめ、失敗したらORIG_HEADかブランチのreflogで戻します。強制pushは--force-with-leaseに限り、mainはブランチ保護で書き換えられないようにしておきます。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。