Gitのcommitの名前やメールアドレスを過去からまとめて変更する

2021/11/25に公開

課題

Gitのcommitの名前やメールアドレスを、過去に遡ってまとめて書き換えたい。

経緯

Githubのリポジトリをprivateからpublicに変更する際に問題が生じた。
過去のcommitの名前やアドレスは公開したくないものだったのだ。
そこで過去に遡って大量のコミット履歴を書き換えることにした。

書き換えたいもの

過去のcommitに存在する下記の4項目を書き換えたい。

  • author name
  • author email
  • committer name
  • committer email

git logのデフォルトでは、authorしか表示されないので注意が必要。
commitの詳細なlogは下記のコマンドなどを試して見て下さい。

git log --all --pretty=full --graph

authorとcommitterの違いについてなどは、公式ドキュメントを参照して下さい。

書き換えたい範囲

あらゆるローカルブランチとあらゆるリモートブランチ。

解決策

書き換えるためのコマンド

下記のコマンドを実行すれば、ローカルブランチのあらゆるコミットを修正することが出来ます。
飽くまでもコマンドの例です。
--env-filterの引数のシェルは必要に応じて変更して下さい。

git filter-branch --force --env-filter '
        # GIT_AUTHOR_NAMEの書き換え
        if [ "$GIT_AUTHOR_NAME" = "変更したいauthor name" ];
        then
                GIT_AUTHOR_NAME="変更後のauthor name";
        fi
        # GIT_AUTHOR_EMAILの書き換え
        if [ "$GIT_AUTHOR_EMAIL" = "変更したいauthor email" ];
        then
                GIT_AUTHOR_EMAIL="変更後のauthor email";
        fi
        # GIT_COMMITTER_NAMEの書き換え
        if [ "$GIT_COMMITTER_NAME" = "変更したいcommitter name" ];
        then
                GIT_COMMITTER_NAME="変更後のcommitter name";
        fi
        # GIT_COMMITTER_EMAILの書き換え
        if [ "$GIT_COMMITTER_EMAIL" = "変更したいcommitter email" ];
        then
                GIT_COMMITTER_EMAIL="変更後のcommitter email";
        fi
        ' -- --all

コマンドについての簡単な説明

git filter-branch:gitの歴史を一括で書き換えるコマンドです。

--force:refs/originalに既にバックアップが存在しても実行するオプションです。

--env-filter:コミット自体の情報(author/committer name/email/timeなど)を修正するためのオプションです。

--:変更の適用範囲を決めるオプションです。範囲は色々変えられるようです。今回は全てのローカルブランチに適用したいので、--allを指定しています。

詳しくは、公式ドキュメントを読んで下さい。

コマンド実行後の状態

ここで下記のコマンドを実行して確認して下さい。

git log --all --pretty=full --graph

全てのローカルブランチ(リモート追跡ブランチも含む)の歴史が書き換えられたことが確認出来ると思います。

同時にrefs/originalに書き換える前の歴史が存在すること気付くかと思います。
これは、git filter-branchを実行する際に、gitが自動で残してくれるバックアップです。
気になる方は下記のコマンドで消して下さい。(貴重なバックアップなのでよく考えてから消して下さいね、、、)

git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d

もう一度、下記のコマンドで結果を確認して下さい。

git log --all --pretty=full --graph

書き換えられた歴史のみが存在することが分かると思います。

リモートブランチに反映させる

あとは,下記のコマンドで全てのローカルブランチをpushすれば、リモートにも反映されます。

git push --all --force origin

ローカルブランチの存在しないリモートブランチには変更が反映されないので注意して下さい。

ローカルリポジトリから改竄の痕跡を抹消する

ここまでの操作で歴史の書き換えには成功しましたが、書き換える前の痕跡はローカルリポジトリ上に残っています。
貴方のリポジトリに限らず全てのローカルリポジトリに言えることです。

例えば、

git checkout 改竄前の歴史のコミットid

で、改竄前の状態をそのまま復元することが出来ます。

本来的なGitの使い方としてはむしろ残すべきでしょう。
ここから先は、改竄の痕跡を抹消する方法を書きます。
詳細については公式サイトをご覧下さい。

痕跡抹消法1

新たにリモートリポジトリからgit cloneし直すことです。
これで痕跡の抹消された無傷のリポジトリを得ることが出来ます。
元々のローカルリポジトリに関しては、バックアップとして残すなり、削除するなり好きにして下さい。

痕跡抹消法2

どうしてもgit cloneし直すことが出来ない場合には、以下の方法があるようです。

公式サイトにあるように、作業前にバックアップを取るか、さもなくば、やめておけ、という破壊的な方法です。
どうしてもgit cloneし直すことが出来ない人は、バックアップをとった上で下記の方法を試してみて下さい。

refs/originalを削除する

refs/originalには改竄前の歴史が存在します。
下記のコマンドで削除することが出来ます。

git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d

もう一度、下記のコマンドで結果を確認して下さい。

git log --all --pretty=full --graph

書き換えられた歴史のみが存在することが分かると思います。

reflogを抹消する

gitのreflogを抹消します。
reflogとは、ブランチやHEADがどのコミットを参照していたのかの履歴です。
ローカルリポジトリに存在する履歴です。

下記のコマンドを実行して下さい。

git reflog expire --expire=now --all

これで過去のreflogを全てが抹消されました。
詳細については公式サイトをご覧下さい。

改竄前のGitオブジェクトを削除する

改竄前のGitオブジェクトを削除します。
簡単に言えば、Gitオブジェクトとはコミットした時のリポジトリの状態のスナップショットです。

reflogの削除は、改竄される以前の歴史年表を削除するようなものです。
今度は、改竄前の歴史そのものを削除します。

下記のコマンドを実行して下さい。

git gc --prune=now

全ての『不要な』Gitオブジェクトを削除するコマンドです。
ここで言う『不要な』とは、『Gitの歴史から辿り着くことの出来ない』という意味です。
reflogを消したことで、改竄される前の歴史は『Gitの歴史から辿り着くことの出来ない』ものになりました。

reflogを削除したのは、git gc --prune=nowに『不要な』Gitオブジェクトを抹消させる準備とも言えます。

詳細については公式サイトをご覧下さい。

これでローカルリポジトリからあらゆる改竄の痕跡が削除されました。
これで改竄の履歴も、履歴が指し示す事実も、全てが消え去ったのです。

リモートブランチとの同期に注意

fetchに注意

git filter-branchの実行に際して、-- --allを指定すれば全てのローカルブランチの歴史は改竄されます。
リモート追跡ブランチの歴史も改竄されます。リモート追跡ブランチはローカルブランチの一種だからです。リモート追跡ブランチはリモートブランチではありません。
git fetchしない限りは、リモート追跡ブランチはリモートブランチに同期されません。

逆に、git fetchしてしまうと、リモート追跡ブランチはリモートリポジトリの状態に同期されます。
則ち、git pushする前に同期してしまえば、リモート追跡ブランチは改竄前の状態に戻ってしまいます。

勿論、Gitオブジェクトも復元され、

git checkout 改竄前の歴史のコミットid

で、改竄前の状態をそのまま復元することが出来ます。
改竄前のあらゆる痕跡を抹消したい人は注意して下さい。

VSCodeなどの開発ツールに注意

VScodeには自動でgit fetchする機能があります。
則ち、自動的に改竄前の状態が復元されてしまいます。

VScodeに限らず便利なツールでGitを管理している人は、この点を注意して下さい。
ツールの設定を変更するなどの解決策があるとは思いますが、terminalを使うのが確実だと思います。

最後に

Gitの歴史を書き換えることは、壊滅的な結果になりかねません。
自分一人で開発している分には、まあいいかも知れません。
しかし、チームで開発している場合は、この壊滅的な作業を実行する前に、絶対にチームの責任者に相談して下さい。

書いているうちにJOJOの第五部のボスのディアボロを思い出しました、、、「何かわからんがくらえッ!」

Discussion