削除したブランチを復活させるGitサブコマンドを書いた

2021/11/21に公開

gitで削除済みのブランチを復活させるサブコマンド git restore-branch を実装したのでその紹介と、周辺情報のまとめ。

実装したロジック

git branch -d branch-nameでローカルブランチを削除すると、

  • head参照 (refs/heads/branch-name)
  • reflog (logs/refs/heads/branch-name)

が削除され、git log branch-namegit reflog branch-name が使えなくなる。

一方、過去のgit checkout操作の記録はHEADのreflogに残っており、現存するブランチであれば git checkout @{-N} で再度チェックアウトすることができる。(参考:gitで○○する前のソースを取得する / チェックアウトする前

git checkout @{-N} はreflogからN番目の"checkout: moving from"を探し、そこからブランチ名を抽出してそのブランチ名でチェックアウトを行う。そのため、削除してもう存在しないブランチはチェックアウトできない。

$ git checkout @{-4}
error: pathspec '@{-4}' did not match any file(s) known to git
$ git checkout @{-4} --
fatal: invalid reference: @{-4}

ただしここで、reflogのレコードが削除されていないという前提を置けば、"checkout: moving from"よりも一つ古いreflogレコードのコミットハッシュは、チェックアウトでブランチを切り替える前のリビジョンだと言える。

そういうわけで、

  • reflogを新しい方から見ていく。
  • チェックアウトのレコードからブランチ名を取得。
  • 現存しているブランチならスキップ。
  • 処理済みのブランチ名ならスキップ。
  • 一つ古いreflogレコードからコミットハッシュを取得。

とすれば、削除されたブランチの最終状態を抽出できる。

このロジックで、削除されたブランチと対応するコミットハッシュを一覧表示し、いずれかのブランチ名を指定して実行するとそのブランチを作り直すサブコマンドを実装した。

https://github.com/yoichi/git-restore-branch

$ git restore-branch
foo = bf32a747a3918b11edf3af511599c51b95a1ebef
bar = 796f4d8294029a07e6c057116520445d85484cc5
$ git restore-branch foo
restored: bf32a747a3918b11edf3af511599c51b95a1ebef -> foo

git resurrect

gitのcontrib以下に、関連するサブコマンドが実は存在していた。

https://github.com/git/git/blob/master/contrib/git-resurrect.sh

ブランチ名を指定して、ブランチを復活させるサブコマンド git resurrect である。

$ git resurrect foo
** Candidates for foo **
bf32a74 [18 minutes ago] test commit
** Restoring foo to bf32a74 test commit

対象コミットを探す際に logs/HEAD を直接参照していて、logs/HEAD の該当行の第一フィールドのコミットハッシュ(そのreflogレコードの操作前のコミットハッシュ値)を使っている。そのため、"checkout: moving from"より一つ古いreflogレコードが削除されていたとしても正しく動作する。git restore-branchでは内部で使っているgit reflogでこのフィールドが参照できなさそうなので制限事項としている。

また、git resurrect ではオプション指定により、reflog のマージ記録やマージコミットのコミットメッセージも検索対象として候補を抽出することもできる。

git restore-branch はreflogで一番新しいものを採用するが、git resurrect は候補のうちコミット日時が一番新しいものを採用するという違いがある。

リモートブランチの場合

ローカルブランチではなく、リモートブランチの削除の場合は事情が異なる。fetchした後checkoutせずにリモートブランチを git push -d origin branch-name で削除してしまった場合、reflogを活用してブランチを復活させることはできない。したがって git restore-branch は無力である。

git resurrect-m (--merges) や -t (--merge-targets) でマージコミットのコミットメッセージから対象コミットを見つけられる場合はそれで復活できる。

一方、対象コミットが全く参照されないコミットになっている場合は、git fsckで参照されていないコミットオブジェクトを列挙しその中から対象を見つけられる場合がある。ただしブランチ名を抽出する汎用的な方法はないので、別途何とかする必要がある。

Discussion