GitのコンフリクトをAIと解く:両側の意図を残す2つの演習
競合マーカーを消せても、必要な機能を片方から落とせば統合は失敗です。AIには先に「両側が何を変えたか」を説明させ、その後で統合案を作らせましょう。ここでは架空の入力検証コードを使って、同じ行の競合と、削除されたファイルへの編集を練習します。
操作は学習用のリポジトリで行います。共有ブランチへの強制pushや、本番の変更を取り消す演習ではありません。履歴管理の基本はAI開発のバージョン管理で確認してください。
始める前に、作業状態と統合の方向を確認する
最初にgit statusで未コミット変更を確認します。未保存の作業があるなら、内容を理解したうえで記録するなど、先に扱いを決めてください。Gitの公式マニュアルも、開始前の変更があると中止時に元の状態を復元できない場合があると説明しています。[出典1]
次に、今いるブランチと取り込むブランチを書きます。「AをBへ取り込む」と「BをAへ取り込む」を混ぜると、ours/theirsを誤解しやすくなります。本記事の説明は通常のmergeを想定し、rebase時の役割へそのまま転用しません。
git status
git branch --show-current
git diff --name-only --diff-filter=U
最後のコマンドは、競合が起きた後に未解決ファイルを確認するものです。まだmergeしていない時点で何も出ないことを、「統合後も競合しない」という証明にはできません。
演習1:空白の除去と文字数制限が同じ行で競合した
架空のタスク名入力を考えます。元の仕様は「空でなければ登録できる」でした。Aの変更は空白だけの入力を拒否し、Bの変更は40文字を超える入力を拒否するものです。競合箇所が次の形になったとします。
<<<<<<< HEAD
return value.trim().length > 0;
=======
return value.length > 0 && value.length <= 40;
>>>>>>> lesson-length-limit
Aだけを残すと上限が消え、Bだけを残すと空白入力を通します。「両方を残す」を選んでも、returnが2つ並べば後半は実行されません。まず統合後の仕様を「文字列を受け取り、前後の空白を取り除いた結果が1〜40文字なら受け付ける」と書いてから、コードを作ります。
export function isValidTaskName(value) {
if (typeof value !== 'string') return false;
const name = value.trim();
return name.length >= 1 && name.length <= 40;
}
ここでの40は教材の仕様です。現実の入力上限を勧める数値ではありません。またJavaScriptのlengthは、画面で見える文字数と一致しない場合があります。絵文字なども扱う実際の仕様なら、文字数の数え方を別途決めてください。
AIへ渡す依頼には、変更意図と禁止する修正を含める
この競合は通常のmergeで発生しています。Aは空白だけの入力を拒否し、Bは40文字を上限にしました。まずそれぞれの変更意図と失われる可能性のある条件を説明してください。統合後はtrimした文字列で1〜40を判定します。テストの削除、上限の変更、入力エラーの無視をせず、最小の統合案と境界値テストを提示してください。
AIの説明が片側の差分を見ていなければ、そのまま編集させず、比較対象を渡し直します。必要に応じて共通の祖先の内容も見ます。競合中のファイルでは、git show :1:パスなどのステージ別の表示を使えますが、利用できる状態とパスを確認してから使ってください。[出典1]
統合案を、成功例と失敗例の両方で確かめる
最終関数をvalidate.mjsへ保存した場合、次の教材テストをvalidate.test.mjsに置き、node validate.test.mjsで実行できます。外部パッケージは不要です。
import assert from 'node:assert/strict';
import { isValidTaskName } from './validate.mjs';
assert.equal(isValidTaskName(''), false);
assert.equal(isValidTaskName(' '), false);
assert.equal(isValidTaskName('教材を読む'), true);
assert.equal(isValidTaskName('x'.repeat(40)), true);
assert.equal(isValidTaskName('x'.repeat(41)), false);
assert.equal(isValidTaskName(123), false);
assert.equal(isValidTaskName(' x '), true);
全部成功しても、既存テストを削除していないか確認します。仕様変更に合わせて期待値を直す場合は、理由を説明できることが条件です。「通らないからテストを緩める」を統合の解決策にしません。
演習2:削除されたファイルへ、もう片方が修正した
次は、Aがlegacy-validation.mjsを削除し、機能をsrc/validate.mjsへ移した場面です。Bは旧ファイルに人数の上限チェックを追加しました。ファイルを復活させればBの差分は残りますが、Aが意図した整理を元に戻してしまいます。
| 確認するもの | 判断する内容 |
|---|---|
| Aの差分 | 削除だけか、移動先へ機能が引き継がれているか |
| Bの差分 | 追加した条件と、その条件を使う呼出元 |
| 統合案 | 旧ファイルを残す必要があるか、移動先へ条件を反映できるか |
| 検証 | 移動先のテスト、importの参照先、旧機能の消失 |
この教材なら、移動先へBの必要な条件を追加し、呼出元とテストの参照を確認する案が考えられます。ただし実際の競合では、ファイル名だけから移動と決め付けません。両側の履歴と差分が判断材料です。
解決済みにする前の最終確認
- 競合したすべての箇所を確認し、マーカーが残っていない。
- AとBの変更目的を、統合後の仕様で説明できる。
- 必要な既存テストが残り、境界値と異常系も確認した。
git diffで予定外の削除・生成物・設定変更がない。- 追加する対象ファイルを確認してからステージし、差分をレビューする。
個別適用や説明の拡張時に確認する事項:GitHubの画面で扱えるのは単純な行競合などで、複雑な競合には別のクライアントやコマンドラインが必要になります。Web上の解決操作がどちらのブランチを更新するかも確認してください。[出典2]
出典
- Git:git-merge公式マニュアル。開始前の状態、競合の表示、解決時の確認。
- GitHub:マージ競合の解決。Web上で解決できる範囲と更新先。