Git入門:バージョン管理のきほん

直前のコミットを直す(commit --amend)

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • git commit --amend でメッセージや内容を直す手順
  • amend が「修正」ではなく「別のコミットへの置き換え」である仕組み
  • push 済みのコミットを amend すると何が壊れるか、その避け方

コミットした直後に気づく

コミットした瞬間に気づくことがあります。メッセージを打ち間違えた。ファイルを 1 つ add し忘れた。デバッグ用の console.log を消し忘れた。

ターミナル

$ git commit -m "ログイン機能を追加" [feature/login 8f3c1a2] ログイン機能を追加 1 file changed, 24 insertions(+) $ git status On branch feature/login Untracked files: src/validator.js

一緒にコミットするはずだった src/validator.js が漏れています。ここで「漏れを追加」という追加コミットを作ると、履歴に意味のない 1 行が残ります。数か月後に履歴を読む人にとっては、ただのノイズです。

こういうときに使うのが git commit --amend です。直前のコミットを作り直します。

メッセージだけを直す

まずいちばん単純な使い方です。ステージに何も置かず、そのまま --amend を実行します。

ターミナル

$ git commit --amend

エディタが開き、1 行目に今のコミットメッセージが表示されます。書き直して保存し、エディタを閉じてください。

ターミナル

[feature/login 4d9b7e1] ログイン画面のバリデーションを追加 1 file changed, 24 insertions(+)

エディタを開かずに済ませたいときは -m を付けます。

ターミナル

$ git commit --amend -m "ログイン画面のバリデーションを追加" [feature/login 4d9b7e1] ログイン画面のバリデーションを追加 1 file changed, 24 insertions(+)

エディタが Vim で開いて出られなくなったら、Esc キーを押してから :wq と入力して Enter です。保存せずに抜けたいときは :q! です。エディタを VS Code に変えるには git config --global core.editor "code --wait" を実行してください。Windows も Mac も同じコマンドで設定できます。

ファイルを足して直す

add し忘れたファイルを含めたいときは、先に git add してから --amend します。

ターミナル

$ git add src/validator.js $ git commit --amend --no-edit [feature/login 6c2a8f5] ログイン画面のバリデーションを追加 2 files changed, 31 insertions(+)

--no-edit は「メッセージはそのままでいい」という指定です。これを付けないとエディタが開きます。ファイルを足すだけならメッセージは変えたくないことが多いので、--no-edit はよく使います。

2 files changed に変わったことを確認してください。1 つのコミットに、本来入るはずだった 2 ファイルがまとまりました。

逆に、入れるつもりのなかったファイルを混ぜてしまったときは、ステージから外してから --amend します。

ターミナル

$ git restore --staged debug.log $ git commit --amend --no-edit [feature/login 5a1e9b3] ログイン画面のバリデーションを追加 2 files changed, 31 insertions(+)

git restore --staged はステージから下ろすだけで、ファイルの中身は消しません。この違いは変更を取り消す(restore)で扱った通りです。

やりたいことコマンド
メッセージだけ直すgit commit --amend -m "新しいメッセージ"
ファイルを足す(メッセージ据え置き)git add <file> してから git commit --amend --no-edit
ファイルを足してメッセージも直すgit add <file> してから git commit --amend
入れすぎたファイルを外すgit restore --staged <file> してから git commit --amend --no-edit
作者名や日付を今の設定に直すgit commit --amend --reset-author --no-edit

amend は書き換えではなく置き換え

ここが本題です。--amend は「直前のコミットを編集する」ように見えますが、実際にやっていることは違います。

コミットは、いったん作られたら中身を変えられません。コミットのハッシュ値(8f3c1a2 のような英数字)は、中身から計算された指紋のようなものだからです。中身が 1 文字でも変われば、別の指紋になります。

--amend が実際にやっているのは、次の 3 手です。

  1. 直前のコミットの内容に、今ステージされているものを合わせて、新しいコミットを作る
  2. 現在のブランチが指す先を、その新しいコミットに移す
  3. 元のコミットは、誰からも指されない状態で残る

HEAD は「今いる場所」を指す目印です。ブランチが移動すれば HEAD も一緒に移ります。ハッシュ値を見比べると、置き換わったことがはっきり分かります。

ターミナル

$ git log --oneline -1 8f3c1a2 ログイン機能を追加 $ git commit --amend -m "ログイン画面のバリデーションを追加" $ git log --oneline -1 4d9b7e1 ログイン画面のバリデーションを追加

8f3c1a2 は消えたのではなく、単に誰からも指されなくなりました。しばらくは git reflog からたどれます。これは消したはずの作業を取り戻す(reflog)で詳しく扱います。

ここから 2 つの性質が導けます。

ひとつは、amend は何度でもやり直せるということです。1 回目の amend で失敗しても、2 回目の amend が同じことをするだけなので、状態が悪化しません。

もうひとつは、amend が対象にできるのは直前の 1 個だけだということです。ブランチが指しているのはいちばん新しいコミットで、amend はそこを差し替えて付け替えるだけの操作だからです。2 つ前を差し替えると、その後ろに連なるコミットの親が全部変わり、鎖を作り直す必要が出てきます。それをやるのが git rebase -i で、rebase で履歴を整えるで扱います。

このモデルが分かっていると、次の節の危険性が理屈で理解できます。

push 済みのコミットを amend してはいけない

自分の手元だけにあるコミットなら、amend は安全です。問題は、すでに git push でリモートに送ったあとです。origin はリモートリポジトリに付けられる既定の名前で、多くの場合は GitHub 上のリポジトリを指します。

push 済みのコミットを amend すると、次のことが起きます。

  • 手元のブランチは、新しいハッシュ値のコミットを指している
  • リモートのブランチは、古いハッシュ値のコミットを指したまま
  • 両者は「同じ内容だが別物のコミット」を持つ、枝分かれした履歴になる

この状態で普通に push すると、Git は拒否します。

ターミナル

$ git push To github.com:example/app.git ! [rejected] feature/login -> feature/login (non-fast-forward) error: failed to push some refs to 'github.com:example/app.git' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart.

ここで焦って git push --force を打つのが、いちばんやってはいけない対応です。リモートの履歴を自分の履歴で上書きするので、その間に同僚が push していた作業ごと消えます。

--force は「今のリモートがどうなっていようと上書きする」という命令です。同僚のコミットが乗っていても関係なく消えます。どうしても強制 push が必要なときは --force-with-lease を使ってください。こちらは「自分が最後に見たときからリモートが変わっていなければ上書きする」という条件付きなので、他人の作業が乗っていれば止まってくれます。

さらに厄介なのは、他の人がすでに古いコミットを pull していた場合です。その人の手元には古いコミットが残り、こちらには新しいコミットがある。次にその人が pull すると、Git は 2 つを別々のコミットとして扱うので、同じ変更が 2 回入った奇妙な履歴ができ上がります。同じ行が 2 度書き換わってコンフリクトになることもあります。

この事故が厄介なのは、amend した本人の手元では何も壊れて見えない点です。異常が出るのは他の人の手元なので、原因の切り分けに時間がかかります。「履歴を作り替える操作は、共有した瞬間から自分だけの問題ではなくなる」と覚えてください。

判断は単純にしてかまいません。

  • push していない → amend してよい
  • push したが、誰も pull していない自分だけのブランチ--force-with-lease で直せる
  • push していて、共有されている(main や、レビュー中のブランチ) → amend しない。公開済みの履歴を安全に戻す(revert)で打ち消す

push 済みかどうかは、git status の先頭で分かります。

ターミナル

$ git status On branch feature/login Your branch is ahead of 'origin/feature/login' by 1 commit. (use "git push" to publish your local commits)

ahead of 'origin/...' by 1 commit は「手元に push していないコミットが 1 個ある」という意味です。この表示が出ている範囲なら、amend しても誰にも影響しません。amend の前に git status を見る、という手順を固定しておくと事故が減ります。

よくある失敗と、そこからの戻し方

いちばん多い失敗は、git add を忘れたまま --amend を実行してしまうことです。

ターミナル

$ git commit --amend --no-edit [feature/login 4d9b7e1] ログイン画面のバリデーションを追加 1 file changed, 24 insertions(+)

ファイルを足したつもりが 1 file changed のままです。ステージが空だったので、内容は何も変わらず、ハッシュ値だけが新しくなりました。

このときはあわてず、git add してからもう一度 --amend すれば済みます。amend は何度でもやり直せます。

ターミナル

$ git add src/validator.js $ git commit --amend --no-edit [feature/login 7e5c3d9] ログイン画面のバリデーションを追加 2 files changed, 31 insertions(+)

もうひとつは、amend でメッセージを消してしまい、元の文面に戻したくなる場合です。git reflog を見ると、amend の前後が両方記録されています。

ターミナル

$ git reflog -3 7e5c3d9 HEAD@{0}: commit (amend): ログイン画面のバリデーションを追加 4d9b7e1 HEAD@{1}: commit (amend): ログイン画面のバリデーションを追加 8f3c1a2 HEAD@{2}: commit: ログイン機能を追加

HEAD@{2} が元のコミットです。ここへ戻せば amend する前の状態に復帰できます。

事故を減らすいちばん確実な方法は、--amend を打つ前に git statusgit diff --staged で、これから何が入るかを目で見て確かめることです。

ターミナル

$ git diff --staged --stat src/validator.js | 7 +++++++ 1 file changed, 7 insertions(+)

ここに何も表示されなければ、ステージは空です。その状態で --amend --no-edit を実行しても、内容は変わらずハッシュ値だけが変わります。

まとめると、amend を安全に使う手順は次の 3 つです。

  1. git status で push 済みかどうかを確かめる
  2. git diff --staged で、これから足す内容を確かめる
  3. git commit --amend を実行し、出力の N files changed が想定通りか読む

3 行とも、打つのに数秒しかかかりません。履歴を作り替える操作の前としては、安い保険です。

この章のポイント
  • git commit --amend は直前のコミット 1 個だけを作り直す。2 つ前は対象にできない
  • amend は編集ではなく置き換え。新しいコミットを作ってブランチの指す先を移している
  • ハッシュ値が変わるので、push 済みのコミットに使うと履歴が食い違って事故になる
  • 共有済みなら amend せず revert を使う。強制 push が要るときは --force-with-lease