Git

マージから失われた2番目の親を復元する

何らかの理由で2番目の親が記録されずマージコミットにならなかったマージを、マージコミットにする方法を考えます。


前提条件


  • マージをやり直さない

  • 親コミット以外、各コミット時点でのリポジトリの内容を変更しない

  • コミットのタイムスタンプを変更しない(Commiter Dateも)

image.png

図のコミットA5はコミットA4にB4をマージしたコミットとします。

何らかの理由でマージコミットにならなかったため、B4-A5の線が消えてしまいました1

これを元に戻していきます。


方法1 rebase

rebase でなんとかするというのはすぐに思いつくと思います(なりません)。

git checkout A

git rebase -i A3

pick A4

exec git merge -s ours B4
fixup A5
pick A6
pick A7



  1. merge-s oursを指定して空のマージコミットを作ります。

  2. そこにA5を融合させることで目的のリビジョングラフを得ることができます。

しかし、この方法ではA5のコミットメッセージおよびタイムスタンプが失われてしまいます


方法2 replace

replace を使って、親コミットだけを変更します。

usage: git replace [-f] <object> <replacement>

or: git replace [-f] --edit <object>
or: git replace [-f] --graft <commit> [<parent>...]
or: git replace -d <object>...
or: git replace [--format=<format>] [-l [<pattern>]]

-l, --list list replace refs
-d, --delete delete replace refs
-e, --edit edit existing object
-g, --graft change a commit's parents
-f, --force replace the ref if it exists
--raw do not pretty-print contents for --edit
--format <format> use this format

次のコマンドラインを使えば親コミットを変更できます。

git replace --graft <commit> [<parent>...]

そこで、次のようにコマンドを実行すると目的のリビジョングラフを得ることができます。

git checkout A

git replace --graft A5 A4 B4
git replace --graft A6 replace/A5
git replace --graft A7 replace/A6
git reset --hard replace/A7
git replace -d A5
git replace -d A6
git replace -d A7


  1. A5の親をA4,B4に変更します。これでマージコミットになります。

  2. A6の親を新しいA5に変更します。A5を置き換えたコミットはreplace/A5で参照できます。

  3. グラフがつながるように親を順次変更します。

  4. 最新のコミットまで置き換えたら、新しいコミットにリセットします。

  5. 最後に置き換えたコミットの参照を消します。

  6. 親コミット以外は何も変更せずに、目的のリビジョングラフを得ることができました。

この方法は rebase と違って作業フォルダで差分を適用するわけではないので、極めて高速です。


スクリプト化

コミットが大量にある場合、手動でコマンドを実行するのは大変です。

良く観察すると次の順に実行しても問題ないことがわかります。

git checkout A

git replace --graft A5 A4 B4

git replace --graft A6 replace/A5
git replace -d A5

git replace --graft A7 replace/A6
git replace -d A6

git reset --hard replace/A7
git replace -d A7

真ん中の部分は繰り返しなので、スクリプト化します(サンプルはPowerShellです)。


rebase-by-replace.ps1

function rebase-by-replace($Commits, $InitialCommit)

{
git replace --graft $Commits[0] $InitialCommit $Commits[0]
for($i = 1; $i -lt $Commits.length; $i++)
{
git replace --graft $Commits[$i] "replace/$($Commits[$i-1])" $Commits[$i]
git replace -d $Commits[$i-1]
}

git reset --hard "replace/$($Commits[$Commits.length-1][0])"
git replace -d $Commits[$Commits.length-1][0]
}


このままだとマージコミットにできないので、とりあえずCSV形式で2番目の親も渡せるようにします。


rebase-by-replace.ps1

function rebase-by-replace($Commits, $InitialCommit)

{
for($i = 0; $i -lt $Commits.length; $i++)
{
$Commits[$i] = $Commits[$i] -split ','
}

git replace --graft $Commits[0][0] $InitialCommit $Commits[0][1]
for($i = 1; $i -lt $Commits.length; $i++)
{
git replace --graft $Commits[$i][0] "replace/$($Commits[$i-1][0])" $Commits[$i][1]
git replace -d $Commits[$i-1][0]
}

git reset --hard "replace/$($Commits[$Commits.length-1][0])"
git replace -d $Commits[$Commits.length-1][0]
}


このスクリプトを使うと前述の操作は次のように実行できます。

git checkout -b new_A A4

rebase-by-replace @("A5,B4","A6","A7") A4





  1. 例えば、間違えてmerge --squashにしてしまったときや、git-svnでSubversionからの移行をして、mergeinfo属性がうまく取り込めなかったときなど。