Git入門:バージョン管理のきほん
直前のコミットを直す(commit --amend)
このレッスンで分かること
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 手です。
- 直前のコミットの内容に、今ステージされているものを合わせて、新しいコミットを作る
- 現在のブランチが指す先を、その新しいコミットに移す
- 元のコミットは、誰からも指されない状態で残る
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 status と git diff --staged で、これから何が入るかを目で見て確かめることです。
ターミナル
$ git diff --staged --stat
src/validator.js | 7 +++++++
1 file changed, 7 insertions(+)ここに何も表示されなければ、ステージは空です。その状態で --amend --no-edit を実行しても、内容は変わらずハッシュ値だけが変わります。
まとめると、amend を安全に使う手順は次の 3 つです。
git statusで push 済みかどうかを確かめるgit diff --stagedで、これから足す内容を確かめるgit commit --amendを実行し、出力のN files changedが想定通りか読む
3 行とも、打つのに数秒しかかかりません。履歴を作り替える操作の前としては、安い保険です。
git commit --amendは直前のコミット 1 個だけを作り直す。2 つ前は対象にできない- amend は編集ではなく置き換え。新しいコミットを作ってブランチの指す先を移している
- ハッシュ値が変わるので、push 済みのコミットに使うと履歴が食い違って事故になる
- 共有済みなら amend せず revert を使う。強制 push が要るときは
--force-with-lease