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

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

# 前提条件

* マージをやり直さない
* 親コミット以外、各コミット時点でのリポジトリの内容を変更しない
* コミットのタイムスタンプを変更しない(Commiter Dateも)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31974/3003b1c6-764c-0369-10f4-db976e9ffd2e.png)

図のコミットA5はコミットA4にB4をマージしたコミットとします。
何らかの理由でマージコミットにならなかったため、B4-A5の線が消えてしまいました[^1]。
これを元に戻していきます。

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

# 方法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` を使って、親コミットだけを変更します。

* [Gitのさまざまなツール - Gitオブジェクトの置き換え](https://git-scm.com/book/ja/v2/Git-のさまざまなツール-Git-オブジェクトの置き換え)

```
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です)。

```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番目の親も渡せるようにします。

```powershell: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]
}
```

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

```powershell
git checkout -b new_A A4
rebase-by-replace @("A5,B4","A6","A7") A4
```