PR提出で差分が変になった!
dev > parent-feature > child-feature のようにブランチを親子関係で積み上げて開発している時、こんな現象に遭遇したことはありませんか?
-
PRの差分がおかしい: 子ブランチから親ブランチへPRを出したのに、親ブランチのコミットや、さらにその上の
devのコミットまで混ざって表示される。 -
Rebaseが終わらない: 親ブランチの最新を取り込もうとして
git rebaseしたら、以前解消したはずのコンフリクトが何度も発生し、無限ループのようになる。 -
ログが重複している:
git logを見ると、同じコミットメッセージが2回ずつ登場している。
この記事では、これらの現象の原因である「汚れた履歴」の正体と、それを最も確実かつクリーンに修復する手順(Reset & Cherry-pick法)を解説します。
原因:親の変更を git pull (Merge) してしまっている
最大の原因は、親ブランチ(dev や parent-feature)の更新を取り込む際に、git pull (または git merge) を使ってしまったことです。
ブランチを積み上げている場合、マージコミットを作成すると履歴が複雑に絡み合ってしまいます。
- 汚れた状態(Mergeを使用): 親の履歴が子に合流し、リベース時にGitが「どれが独自の変更か」を判別できなくなります。これが重複コミットやRebaseループの原因です。
- 理想の状態(Rebaseを使用): 親の更新の「上に」、子の変更を積み直すことで履歴は一直線になります。
対処法:履歴の再構築(Reset & Cherry-pick)
もし履歴が汚れてしまい、通常の rebase ができなくなった場合は、「ブランチを一旦クリーンな状態にリセットし、必要なコミットだけを手動で積み直す」のが最短の解決策です。
手順
ここでは例として、以下の構成で feature ブランチを修復します。
-
親ブランチ:
parent-feature -
作業ブランチ:
child-feature
1. 作業中のRebaseを中止する
もしRebaseの泥沼にハマっている場合は、まず中止して元の状態に戻ります。
git rebase --abort
2. 救出したいコミットを特定する
feature ブランチ独自の作業コミット(残したいコミット)のIDを控えます。
# 親ブランチとの差分ログを表示
git log parent-feature..child-feature --oneline
表示されたリストの中から、自分が作業したコミットのIDをメモ帳などにコピーしてください。
※ Merge pull request... などのマージコミットは無視してください。
3. 親ブランチを最新化・クリーンにする
土台となる親ブランチも汚れている可能性があるため、まずは親を完璧な状態にします。
# 親ブランチに移動
git switch parent-feature
# 親の親(devなど)の最新状態を取得
git pull origin dev
# 親ブランチをリベース(または同様の手順で再構築)
git rebase dev
git push --force-with-lease origin parent-feature
4. 作業ブランチを強制リセットする
ここがポイントです。feature ブランチの汚れた履歴をすべて捨て、クリーンになった親ブランチと全く同じ状態にリセットします。
git switch feature
git reset --hard parent-feature
5. コミットを積み直す (Cherry-pick)
手順2で控えたコミットIDを、古い順(ログの下から上)に1つずつ適用します。
git cherry-pick <一番古いコミットID>
git cherry-pick <その次のコミットID>
# ...
もしコンフリクトが発生したら、通常通り解消してください。履歴がクリーンなので、過去の不要なコンフリクトは発生しません。
# コンフリクト解消後
git add .
git cherry-pick --continue
6. 強制プッシュする
履歴を書き換えたため、通常のPushはできません。安全な強制プッシュを使用します。
git push --force-with-lease origin feature
再発防止:今後の運用
今後、親ブランチの変更を取り込む際は、以下の内容に気をつけると良いと思います。
-
devブランチ以外での
git pull origin <親ブランチ>は禁止
マージコミットを作らないようにします。 -
必ず
git rebaseを使う
履歴を一直線に保ちます。 -
親から順番にリベースする
dev>parent>childの順で、根元から最新化していきます。
正しい更新手順の例
# 1. 一番親(dev)を最新化
git switch dev
git pull origin dev
# 2. 中間(parent)をリベース
git switch parent
git rebase dev
git push --force-with-lease origin parent
# 3. 子(child)をリベース
git switch child
git rebase parent
git push --force-with-lease origin child
トラブルシューティング
Q. git reset --hard で作業中のコミットが消えてしまった!
A. git reflog で復活できます。
git reflog コマンドを実行すると、過去の操作履歴が見られます。戻りたい時点の HEAD@{n} を探して、git reset --hard HEAD@{n} すれば元通りです。
Q. Pushしようとしたら (stale info) と出て拒否された
A. リモートの情報が古い状態です。
git fetch origin を実行して最新情報を取得してから、再度 git push --force-with-lease を実行してください。
Q. PRの差分がまだ多い(コミット数がおかしい)
A. PRのターゲットブランチ(Base)が間違っていませんか?
GitHub上でPRのBaseが dev ではなく、直近の親である parent ブランチになっているか確認してください。また、親ブランチ自体が古い場合は、親ブランチをPushしてGitHub側を最新にする必要があります。
まとめ
devブランチからparentブランチを切り、そこから作業ブランチを切る場合には、特に履歴の管理には注意が必要です。「困ったら reset して cherry-pick で積み直す」が大切ですね。