はじめに
「git commit --amendは使えるけど、それ以外の修正方法はよくわからない」
「pushした後のコミット、どうやって直すの…?」
業務でGitを毎日使っていて、リモートにプッシュしたコミットで誤字があってこれ無視してもいいけどなんか治す方法ないのと検索したらなんかできた話。
この記事では、コミットの修正を「ローカル or リモート」「直前 or 数個前」の2軸で整理して、ある程度状況によってどうするかをわかるようにまとめました。最後に紹介するreflogも使えるので載せています。
この記事の対象読者
-
git commitとgit pushは日常的に使っている -
--amendは知ってるが、それ以外の修正方法に自信がない -
rebase -iに苦手意識がある、または使ったことがない - 一度pushしたコミットの修正で困ったことがある
普通のコミット
最もシンプルなコミット
# ステージング → コミット(特に言うことない)
git add .
git commit -m "機能Aを実装"
記事を書くのに気づいたオプションコマンドなど
# add + commit を一発で(追跡済みファイルのみ)
git commit -am "修正を反映"
# 空コミット
git commit --allow-empty -m "空のコミットです"
# コミットメッセージをエディタで書く
git commit # -m なしで実行するとエディタが開く
-amは便利ですが、新規ファイル(untracked)は対象外という点に注意。git add .を省略できるのは、すでにGitが追跡しているファイルだけです。
ローカルでの修正
まだpushしていないコミットは何とでも修正が聞く
直前のコミットを修正する ― --amend
よく使うやつ。「コミットメッセージタイプミスした」「ファイル入れ忘れた」のときに。
# メッセージだけ修正
git commit --amend -m "正しいメッセージ"
# ファイルを追加してコミットし直す
git add forgotten-file.ts
git commit --amend --no-edit # メッセージはそのままファイルだけ追加される
注意: --amendは「前のコミットを編集する」のではなく、「前のコミットを捨てて新しいコミットを作る」操作です。コミットハッシュが変わります。リモートに既にプッシュした時のコミットで気をつけてほしい。
直前のコミットを取り消す ― reset
「そもそもこのコミット自体なかったことにしたい」場合。
# コミットを取り消し、変更はステージングに残す
git reset --soft HEAD~1
# コミットを取り消し、変更はワーキングツリーに残す(ステージング解除)
git reset --mixed HEAD~1 # --mixed はデフォルトなので省略可
# コミットを取り消し、変更も全て破棄
git reset --hard HEAD~1
それぞれの違いとしては:
| モード | コミット | ステージング | ワーキングツリー |
|---|---|---|---|
--soft |
取り消す | 残る | 残る |
--mixed |
取り消す | 取り消す | 残る |
--hard |
取り消す | 取り消す | 消える |
--hardは変更が本当に消えます。もし間違えても後述のreflogでなんとかできます。
数個前のコミットを修正する ― rebase -i
例えば「3つ前のコミットメッセージを直したい」「途中のコミットをまとめたい」など。
# 直近3つのコミットを対象にリベースをする
# それぞれのコミット文をリベースしながら書き直すイメージ
git rebase -i HEAD~3
デフォルトのエディタがvimの場合、こんな画面が開きます:
pick a1b2c3d 機能Aの実装
pick d4e5f6g typoを修正
pick h7i8j9k テストを追加
# Rebase xxxxxxx..xxxxxxx onto xxxxxxx
#
# 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
# d, drop = remove commit
vimの基本操作(これだけ覚えればOK)
vimに慣れていない人(私です)は何も教えて進まないと思うのですが、rebase -iで使う操作は以下の4つだけで:
| やりたいこと | キー操作 |
|---|---|
| 文字を編集する |
iを押す(挿入モードに入る) |
| 編集を終える |
Escを押す(ノーマルモードに戻る) |
| 保存して閉じる |
Esc → :wq → Enter
|
| 保存せず中止する |
Esc → :q! → Enter
|
つまり、iで編集モードに入ってpickをfixupなどに書き換えて、Esc → :wq → Enterで確定。これだけです。
💡 vim以外のエディタを使いたい場合
# VSCodeを使う場合 git config --global core.editor "code --wait"
ここで各コミットに対して指示を出します:
| コマンド | 意味 |
|---|---|
pick |
そのまま使う |
reword |
メッセージを変更 |
edit |
コミット内容を修正 |
squash |
前のコミットと統合(メッセージ編集あり) |
fixup |
前のコミットと統合(メッセージは前のを使用) |
drop |
コミットを削除 |
例:typo修正コミットを前のコミットに統合する
pick a1b2c3d 機能Aの実装
fixup d4e5f6g typoを修正 ← pick → fixup に変更
pick h7i8j9k テストを追加
保存してエディタを閉じると、d4e5f6gがa1b2c3dに吸収されて、履歴がきれいになります。
rebase -i で気をつけること
-
コンフリクトが起きることがある。
git rebase --continueで続行、git rebase --abortで中止 - リベース中に「もうやめたい」と思ったら
--abortで元に戻せる - 表示順は古い順(上が古い)。
git logの逆
リモートpush後の修正
Gitの「ブランチ」は、特定のコミットを指すポインタです。git pushすると、リモートのポインタが更新されます。
ローカル: main → コミットC → コミットB → コミットA
リモート: main → コミットC → コミットB → コミットA
↑ 同じものを指している
ここでローカルのコミットをamendやrebaseで書き換えると:
ローカル: main → コミットC' → コミットB → コミットA (C'は新しいコミット)
リモート: main → コミットC → コミットB → コミットA (古いCのまま)
この状態だとgit pushしても拒否されます。コミットのハッシュが異なっているからとか。
リモートでも履歴を整理したい ― rebase -i + force-push
わたしはこれになってしまったのですが、「feature branchにpushした後にコミットのタイポに気づいてなんとかなおしたい」。PRのレビュー前に履歴をきれいにしたい場合、リモートでもrebase -iは使えます。
# ① ローカルでrebase -iを実行
git rebase -i HEAD~3
# ② vimで編集して保存(:wq)
# ③ リモートの履歴を上書き
git push --force-with-lease
ローカルのセクションで紹介したrebase -iと操作は同じで、最後に--force-with-leaseでpushするだけ。
打ち消しで対応する ― revert
revertは「過去のコミットを打ち消す新しいコミットを作る」操作です。
# 直前のコミットを打ち消す
git revert HEAD
# 特定のコミットを打ち消す
git revert <commit-hash>
# 複数コミットを一括で打ち消す
git revert HEAD~3..HEAD
修正前: A → B → C(問題のコミット)
修正後: A → B → C → C'(Cを打ち消すコミット)
最後の命綱 ― reflog
ここまでの操作をすべて台無しにしてぐちゃぐちゃになってもreflogが助けてくれる。
reflogとは
reflog`は「あなたがGitに対して行った全操作の記録」です。
commit、reset、rebase、amend、checkout、merge…HEADが動くたびに記録されています。
git reflog
a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/feature
x9y8z7w HEAD@{1}: rebase (pick): テストを追加
m3n4o5p HEAD@{2}: rebase (start): checkout HEAD~3
f4e5d6c HEAD@{3}: commit: テストを追加
b2c3d4e HEAD@{4}: commit: 機能Aの実装
なぜreflogで「消えた」コミットを探し出せるのか
Gitはコミットを本当に削除することはほぼありません。
reset --hardしても、rebaseでsquashしても、元のコミットオブジェクト自体はリポジトリに残っています。ただ、どこからも参照されなくなっただけ。
reflogはHEADの移動履歴を全て持っているので、「参照されなくなったコミット」がどこにあるのかを教えてくれる。
通常のgit log(コミットツリー):
main → C' → B → A
(reset --hard で C が見えなくなった)
reflog(操作ログ):
HEAD@{0}: reset: moving to HEAD~1 ← 今ここ
HEAD@{1}: commit: 機能Cの実装 ← Cはまだ存在する!
HEAD@{2}: commit: 機能Bの実装
rebase -i をやらかした場合の救出
# rebase -i で履歴を整理しようとした
git rebase -i HEAD~3
# ...なんかコミットが消えた気がする
git reflog
# rebase前の状態を見つける(rebase (start) の1つ前)
# 例: HEAD@{5} が rebase 前の状態だった場合
git reset --hard HEAD@{5}
# → rebase前の状態に復元できる
reset --hard をやらかした場合の救出
# うっかり直前のコミットを消してしまった
git reset --hard HEAD~1
# reflogで消えたコミットを探す
git reflog
# HEAD@{1} が reset 前の状態
git reset --hard HEAD@{1}
# → 復元
reflogの注意点
- ローカル限定。リモートには存在しない
-
デフォルトで90日間保持。
git gcで古いものは削除される - cloneし直すと消える。reflogはそのリポジトリのローカル操作ログなので
-
git gcが手動実行されると、参照されていないコミットが本当に消える可能性がある
reflogは万能ではないけれど、「やらかした直後」であればほぼ何とかなるコマンドです。
まとめ ― コミット修正の早見表
| やりたいこと | ローカル(push前) | リモート(push後) |
|---|---|---|
| 直前のメッセージ修正 | commit --amend |
revert + 新コミット |
| 直前にファイル追加 |
add → commit --amend
|
revert + 新コミット |
| 直前のコミット取り消し | reset --soft HEAD~1 |
revert HEAD |
| 数個前のコミット修正 | rebase -i |
revert <hash> or rebase -i + force-push※ |
| コミット統合 |
rebase -i (squash/fixup) |
rebase -i + force-push※ |
| 全部やらかした |
reflog → reset --hard
|
— |
株式会社シンシアでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら
シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。