◆ 1 ブランチなら単純にリベース
◆ 複数ブランチだとリベース先を考えながらリベースすればできる
◆ 一時ブランチに全部をマージして preserve-merges 設定して rebase すると構造が保持される
  ◆ リベース先にブランチを移動して 一時ブランチを消せば完成
  ◆ この処理も自動でするようにした

git のツリーですでにあるコミットとコミットの間に新しいコミットをはさみたいです

1 ブランチなら

ブランチが一つなら はさみたいところの親コミットから分岐してはさみたいコミットを作って ブランチをそこへリベースすればいいです

--A--B--C-->



Z
/
--A--B--C-->



Z--B--C-->
/
--A

しかし はさみたい場所以降に多数のブランチがある場合は簡単には行きません

ブランチが複数あるとき

こういう枝分かれしてるツリーがあります
アルファベットがコミットで () の数字がブランチです

*      H (4)
| * G (3)
|/
* F
| * E (2)
| | * D (1)
| |/
| * C
|/
* B
* A

いろいろ試すために何度も作ったので自動で作れるスクリプト化したものもあります

#!/bin/sh

function commit() {
touch $1
git add -A
git commit -m $1
}

git init
commit A
git branch 1
git checkout 1
commit B
git branch 3
commit C
git branch 2
commit D
git checkout 2
commit E
git checkout 3
commit F
git branch 4
commit G
git checkout 4
commit H

このツリーの A と B の間にコミットを入れたいです

X を A から追加して

*        X (n)
| * H (4)
| | * G (3)
| |/
| * F
| | * E (2)
| | | * D (1)
| | |/
| | * C
| |/
| * B
|/
* A

B 以降を X の後ろに持ってきたいです

*      H (4)
| * G (3)
|/
* F
| * E (2)
| | * D (1)
| |/
| * C
|/
* B
* X (n)
* A

単純リベース

単純に全ブランチを n (X のコミット) にリベースするとこうなります

*    H (4)
* F
* B
| * G (3)
| * F
| * B
|/
| * E (2)
| * C
| * B
|/
| * D (1)
| * C
| * B
|/
* X
* A

ばらばらになります
B が全部にあります

リベース先を調整すれば

リベース先を工夫すれば

git rebase n 1
git rebase (新しいCのハッシュ) 2
git rebase (新しいBのハッシュ) 3
git rebase (新しいFのハッシュ) 4

*      H (4)
| * G (3)
|/
* F
| * E (2)
| | * D (1)
| |/
| * C
|/
* B
* X (n)
* A

うまくできました
しかし ツリーを見てどこにリベースすべきとか考えるのは大変ですし ブランチが多かったり 追加を何度もやることになると……
さすがにどうにかしたいです

一時ブランチにマージしてリベース

考えているとそういえばリベースには preserve-merges という機能がありました
リベースするときに 枝分かれしてマージされた情報も保持してリベースしてくれるものです
ただ これはマージされてるものをリベースしたときにしか意味がありません
なら 一時的にマージすればいいやと やってみました

git checkout 1
git branch 0
git checkout 0
git merge 2 3 4
git rebase --preserve-merges n

*---.    Merge branches '2', '3' and '4' into 0 (0)
|\ \ \
| | | * H
| | * | G
| | |/
| | * F
| * | E
* | | D
|/ /
* | C
|/
* B
* X (n)
| * H (4)
| | * G (3)
| |/
| * F
| | * E (2)
| | | * D (1)
| | |/
| | * C
| |/
| * B
|/
* A

うまくツリー構造が保持されています
あとはブランチをリベース先に移動させて一時的なマージブランチである 0 を消せば完了です
0 ブランチにマージするとき競合があっても どうせ消すものですから 無視してマージしてしまっても大丈夫です

*      H (4)
| * G (3)
|/
* F
| * E (2)
| | * D (1)
| |/
| * C
|/
* B
* X (n)
* A

ブランチ移動なども自動でやりたい

ここまでできると ブランチを新しいリベース先に移動させて 0 ブランチの削除までやってしまいたいです

shellscript 力がなくて複雑なことはできる気がしないので python ですることにしました
これまでのマージしてリベースする部分も含んでいます
プログラムから実行するとユーザとメールアドレスにいつものを使ってくれなかったので config で指定しています

import subprocess

branches = ["1", "2", "3", "4"]
rebaseto = "y"
tmpbranch = "0"
config = "-c user.name=dummy -c user.email=dummy@mail"

def runp(cmd):
return subprocess.run(cmd, check=True)

def run(cmd):
return subprocess.run(cmd, check=True, capture_output=True).stdout.decode("utf8")

runp(f"git checkout {branches[0]}")
runp(f"git branch {tmpbranch}")
runp(f"git checkout {tmpbranch}")
runp(f"git {config} merge {' '.join(branches)} --no-edit")
runp(f"git {config} rebase --preserve-merges {rebaseto}")

new_branches = run(f"git show --format=%P {tmpbranch}").split()
new_branches = [(b, run(f"git show {b} --format=%s -q").strip()) for b in new_branches]

old_branches = [(b, run(f"git show {b} --format=%s -q").strip()) for b in branches]

if len(old_branches) != len({b[1] for b in old_branches}):
print("同じコミットメッセージが存在するためブランチの移動は行なえません")
exit()

print("ob", old_branches)
print("nb", new_branches)

for ob in old_branches:
for nb in new_branches:
if ob[1] == nb[1]:
runp(f"git branch -f {ob[0]} {nb[0]}")

runp(f"git checkout {branches[0]}")
runp(f"git branch -D {tmpbranch}")

移動させたい全部のブランチをマージしているので リベース後のマージブランチの各親コミットが新しいブランチにするコミットになります
現在のブランチのコミットとコミットメッセージを比較して同じものならそこに移動させます
同じコミットメッセージがあると判断できないのでブランチ移動はされません