何らかの理由で2番目の親が記録されずマージコミットにならなかったマージを、マージコミットにする方法を考えます。
前提条件
- マージをやり直さない
- 親コミット以外、各コミット時点でのリポジトリの内容を変更しない
- コミットのタイムスタンプを変更しない(Commiter Dateも)
図のコミット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
-
merge
に-s ours
を指定して空のマージコミットを作ります。 - そこに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
- A5の親をA4,B4に変更します。これでマージコミットになります。
- A6の親を新しいA5に変更します。A5を置き換えたコミットは
replace/A5
で参照できます。 - グラフがつながるように親を順次変更します。
- 最新のコミットまで置き換えたら、新しいコミットにリセットします。
- 最後に置き換えたコミットの参照を消します。
- 親コミット以外は何も変更せずに、目的のリビジョングラフを得ることができました。
この方法は 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です)。
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番目の親も渡せるようにします。
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
-
例えば、間違えて
merge --squash
にしてしまったときや、git-svn
でSubversionからの移行をして、mergeinfo属性がうまく取り込めなかったときなど。 ↩