マージコミットによる意図しないファイル削除の解決方法
開発チームで作業していると、マージコミットによって意図しないファイルの削除が発生することがある。
特に、プルリクエストが既にマージされ、リモートリポジトリにプッシュされた後にこの問題に気づくケースは厄介である。
本記事では、実際に遭遇したこのような状況と、その解決方法について詳述する。
発生した現象
今回遭遇した問題は以下のような状況であった。
- 新規機能のプルリクエストを作成し、既にリモートリポジトリにプッシュしていた
- マージコミットにおいて競合を解決した際、すでに存在している機能関連のファイルが誤って削除された
- その後、削除に気づいてリバートコミットを作成したが、依然として必要なファイルが存在しない状態が続いていた
この状況では、新機能は実装されているものの、既存の機能が完全に失われていた。
原因
この問題の根本的な原因を分析すると、以下の要因が重なったことが判明した。
1. 不適切な競合解決
マージコミット作成時に、Gitの競合解決プロセスで削除すべきでないファイルを削除として扱ったことが直接的な原因である。
これは、異なる機能ブランチ間でのマージにおいて、一方のブランチに存在しないファイルを「削除された」と誤認識したためと考えられる。
2. コミット履歴の複雑化
以下のようなコミット履歴の流れが問題を複雑化させた。
f67173d → b42a951(競合解決) → 2fdd8f6(リバート)
リバートコミットはb42a951
の変更のみを取り消すため、元々存在していたファイルの復元は行われない。
これがリバート後も問題が解決しなかった理由である。
3. ブランチ戦略の課題
新機能と既存の機能という、本来独立して開発されるべき機能が同一のリポジトリ状態で競合を起こしたことも、問題の一因である。
解消方法
この問題を解決するために、以下の手順を実行した。
重要な点は、既存のプルリクエストを維持しながら修正を行うことである。
ステップ1: 問題の特定と分析
まず、現在のブランチ状態と削除されたファイルを正確に把握した。
# 現在のコミット履歴を確認
git log --oneline -n 10
# mainブランチとの差分を確認して削除されたファイルを特定
git diff main HEAD --name-status
この分析により、既存機能関連の14個のファイルが削除されていることが判明した。
ステップ2: 修復用ブランチの作成
既存のプルリクエストブランチから修復用の新しいブランチを作成した。
git checkout 作業ブランチ
git checkout -b fix/restore-files
ステップ3: ファイルの復元
mainブランチから削除されたファイルを選択的に復元
した。これがこの解決方法の核心部分である。
git checkout main -- \
ファイル1 \
ファイル2 \
ファイル3
コマンドの詳細解説
このコマンドは、Gitのcheckout
コマンドの特殊な使用法である。
通常のブランチ切り替えとは異なり、特定のブランチから指定したファイルのみを現在のブランチにコピーする機能を持つ。
構文の説明:
git checkout <ブランチ名> -- <ファイルパス>
-
--
(ダブルダッシュ): ブランチ名とファイルパスを明確に区別するセパレータ -
\
(バックスラッシュ): 複数行にわたってコマンドを記述するための行継続文字
動作の詳細:
- 現在のブランチは変更されず、HEAD位置も移動しない
- 指定されたファイルが現在のブランチに存在する場合、mainブランチの内容で完全に置き換えられる
- 指定されたファイルが現在のブランチに存在しない場合、mainブランチから新規作成される
- 取得されたファイルは自動的にステージング領域(インデックス)に追加される
実用的な利点:
- 必要なファイルのみを復元でき、不要な変更を持ち込まない
- ブランチ全体をマージする必要がなく、競合のリスクを最小化できる
- 大量のファイル変更がある中で、特定のファイルのみを対象にできる
このコマンドにより、mainブランチの状態から必要なファイルのみを現在のブランチに復元できる。
ステップ4: 元のプルリクエストへの統合
修復したファイルを元のプルリクエストブランチにマージした。
git checkout 作業ブランチ
git merge fix/restore-files
ステップ5: リモートリポジトリへの反映
最後に、修正された変更をプッシュしてプルリクエストを更新した。
git push origin 作業ブランチ
この手順により、新機能と既存機能の両方が共存する状態を実現できた。
まとめ
今回の問題は、Gitのマージ操作における競合解決の不備と、コミット履歴の複雑化が原因で発生した。
解決策の有効性
採用した解決方法は、以下の点で優れていると考える。
-
git revert
や強制的なreset操作と異なり、全ての変更履歴が保持される - 既存のプルリクエストを閉じることなく修正できる
普段から雰囲気でgitを使っていることを改めて、実感した。
より適切な方法があれば、ぜひ教えてほしい...。
Discussion