git rebase して git push -f すると何が起こるか

# git rebase して git push -f すると何が起こるか

禁じ手の `git push -f` だけど、
「コミット履歴がなんかぶっ壊れて直せなくなるから駄目」くらいの認識しかしてなかったので、
rebaseからの流れで中身がどう変化するのかちょっと調べてみた。

## 検証用にリポジトリを準備

```bash:bash
cd /tmp
(mkdir repos.git && cd repos.git && git init --bare)

git clone repos.git work && cd work
```

## ブランチ作ってみる

```bash:bash
# masterブランチ
git commit --allow-empty -m "init"
git commit --allow-empty -m "first"
git commit --allow-empty -m "second"
git push origin master

# ブランチb1を分岐
git checkout -b b1
git commit --allow-empty -m "third"
git push origin b1

echo "(1)--------------------------------------------"
git show-branch -a --sha1-name
git merge-base master b1
echo "--------------------------------------------(1)"
```

みたいにやると、ブランチの履歴はこう。

```bash:output
(1)--------------------------------------------
* [b1] third
 ! [master] second
  ! [origin/b1] third
   ! [origin/master] second
----
* +  [0ed34a9] third
*+++ [b06e7f4] second
b06e7f45ea0aed4650b763d7c88d6e57d1966967
--------------------------------------------(1)
```
`b06e7f4`のコミットから分岐してるのがわかる。

## 親ブランチを壊す

とりあえず履歴を壊せればいいので、
紙面の都合上ここでは`git rebase`じゃなくて`git reset`で。

```bash:bash
git checkout master
git reset --hard HEAD^^
git commit --allow-empty -m "force1"
git commit --allow-empty -m "force2"
git push -f origin master

echo "(2)--------------------------------------------"
git show-branch -a --sha1-name
git merge-base master b1
echo "--------------------------------------------(2)"
```

すると、

```bash:output
(2)--------------------------------------------
! [b1] third
 * [master] force2
  ! [origin/b1] third
   ! [origin/master] force2
----
 * + [5031219] force2
 * + [0b24ee6] force1
+ +  [0ed34a9] third
+ +  [b06e7f4] second
+ +  [246fe02] first
+*++ [07fe638] init
07fe6387710019bcbd90b8a3c458f6a218522ede
--------------------------------------------(2)
```

b1ブランチの分岐元が `07fe638`まで遡る。

まぁ、そりゃそうだ。
元々のb1の分岐元の`b06e7f4`がmasterブランチ上から消えてしまったのだから。

## この状態で子ブランチでrebaseすると

```bash:bash
git checkout b1
git rebase origin/master

echo "(3)--------------------------------------------"
git show-branch -a --sha1-name
git merge-base master b1
echo "--------------------------------------------(3)"
```

```bash:output
(3)--------------------------------------------
* [b1] force2
 ! [master] force2
  ! [origin/b1] third
   ! [origin/master] force2
----
  +  [0ed34a9] third
  +  [b06e7f4] second
  +  [246fe02] first
*+ + [5031219] force2
*+ + [0b24ee6] force1
*+++ [07fe638] init
50312197fff7a0cd77809b56772ff66e2b14f46a
--------------------------------------------(3)
```

分岐元ごと壊した親の方に追従してしまう。

壊す前の親ブランチからの、一連のコミットは全て無かったことになる。
`246fe02` `b06e7f4` はもとより、b1ブランチでコミットした `0ed34a9` まで消えてしまう。

## じゃあmergeだとどうなるか

```bash:bash
git reset --hard ORIG_HEAD  # rebase前に戻す
git merge origin/master

echo "(4)--------------------------------------------"
git show-branch -a --sha1-name
git merge-base master b1
echo "--------------------------------------------(4)"
```

とすると、

```bash:output
(4)--------------------------------------------
* [b1] Merge branch 'master' into b1
 ! [master] force2
  ! [origin/b1] third
   ! [origin/master] force2
----
-    [6b02716] Merge branch 'master' into b1
*+ + [5031219] force2
*+ + [0b24ee6] force1
* +  [0ed34a9] third
* +  [b06e7f4] second
* +  [246fe02] first
*+++ [07fe638] init
50312197fff7a0cd77809b56772ff66e2b14f46a
--------------------------------------------(4)
```

今度は消えたはずの`246fe02`と`b06e7f4`が復活してしまっている。
`0ed34a9`は`246fe02`と`b06e7f4`からの流れのコミットなので、
mergeで`0ed34a9`だけ取り出す、みたいなことはできない。

どうしてもやるなら、`git rebase origin/master`で(3)の状態にしてから、
`git cherry-pick 0ed34a9`みたいにやらないとならない。

## 結局

たいていこういうケースって、
「親ブランチが更新されたので、追従しておこうかな」なパターンで

```bash:bash
git checkout 子ブランチ
git rebase 親ブランチ
```

とかすると思う。
で、親ブランチが壊されてた時に悲惨なことになる。

本質的には、

**公開済みのブランチにforce pushするのがまずい**

というより、

**公開済みのブランチに対してコミット履歴書き換えることがまずい**

ってこと。

まぁ、rebase した時点で fast foward じゃなくなるから、
結局 force じゃないと push できないんだけど。