競合とレビュー|コマンドライン・Git・変更管理

このページはプロモーション(広告)を含みます。

コマンドライン・Git・変更管理(中級・読む35分+演習40分)

この授業のゴール

仕事でどう使うか

2人が同じ設定値を別々の理由で変えると、Gitはどちらが正しいか判断できず、競合(conflict:コンフリクト)として人に判断を求めます。競合は事故ではなく、「意図がぶつかっている」というお知らせです。また、チームでは変更を本線に入れる前に、他の人に差分を確認してもらうレビューを行います。

路線 この授業の使いどころ
DS 同じ集計SQLを2人が直したときに、両方の条件(期間・除外ルール)を正しく残して解決する
ML 設定ファイル(学習率・特徴量リスト)の競合を、実験結果の根拠に基づいて解決し、理由を記録する
DX 業務ルールの変更をレビューで承認し、「誰が・なぜ認めたか」を残す
RS 共著者の原稿修正の衝突を、双方の意図を確認して統合し、解析コードの変更はレビューを経て取り込む

前提

前提:

模擬Git(ブラウザ内)はファイル単位で競合を判定し、ファイル全体を競合マーカーで囲みます。本物のGitは競合した塊(hunk)だけを囲みます。マーカーの形と解決の手順は同じです。

身近な状況:同じ閾値を、反対の理由で変えた

異常検知の設定ファイル config.txt で、共通の祖先では threshold=0.5 でした。

人 ブランチ 変更 理由
ケンさん main threshold=0.6、min_rows=80 異常の見逃しが多いと店舗から指摘
ハナさん feature/threshold threshold=0.7、exclude_test_stores=true を追加 誤検知が多い。テスト店舗のデータが混ざっていた

ハナさんが main に feature/threshold をマージすると、threshold の行を両方が別々の値に変えているので競合します。ここで「自分の0.7を採用」とすると、ケンさんの「見逃し対策」という意図が黙って消えます。「ケンさんの0.6を採用」とすると、ハナさんのテスト店舗除外まで消えるかもしれません。

正しい解決は、両方の意図を確かめて、最終的な内容を決めることです。この例では2人で話し、「テスト店舗を除外すれば誤検知の多くは消えるので、閾値はケンさんの0.6にする。最低行数80とテスト店舗除外はどちらも残す」と決めました。

直感的な説明:Gitは推測しない

3方向マージで「片方だけが変えた」部分は、Gitが自動で取り込みます。「両方が別々に変えた」部分だけは、どちらが正しいか内容の意味を知らないGitには判断できないので、両方の版をファイルに並べて書き、人に渡します。これが競合マーカーです。

解決とは、次の4つのどれかを、理由を持って選ぶことです。

  1. 自分の版を採用する
  2. 相手の版を採用する
  3. 両方を組み合わせる(今回のように、片方の値と他方の追加行を残す)
  4. どちらでもない新しい内容に書き直す

たとえと実際の違い:「推測しない」は安全のための性質ですが、Gitは意味までは見ていません。行の位置が離れていれば、意味的に矛盾する変更(例:片方が列名を変え、もう片方が古い列名を使う行を追加)でも自動で合流します。競合が出なかったからといって正しいとは限らず、マージ後のテストやレビューが必要です。

正確な定義:競合マーカーと解決の手順

競合したファイルには、次のような競合マーカーが書き込まれます。

<<<<<<< HEAD
threshold=0.6
=======
threshold=0.7
>>>>>>> feature/threshold
部分 意味
<<<<<<< HEAD から ======= まで 自分(今いるブランチ、ours)の版
======= から >>>>>>> feature/threshold まで 相手(取り込むブランチ、theirs)の版
マーカーの外 競合していない部分(自動で合流済み)

git config --global merge.conflictStyle diff3 を設定すると、||||||| の後に共通の祖先の版も表示され、「誰が何から何に変えたか」が読みやすくなります。

解決の手順(実Git)

手順 コマンド・操作 確認すること
1 git status both modified: に競合したファイルが並ぶ
2 ファイルを開き、両方の版と理由(git log のメッセージ、相手への確認)を読む 両方の意図
3 最終的な内容に書き換え、マーカー3種をすべて消して保存 マーカーが残っていない
4 git add ファイル 「解決した」という印。status から both modified が消える
5 git commit -m "理由" 親が2つのマージcommitができる
迷ったら git merge --abort マージを始める前の状態に戻る(未commitの変更が無い状態で始めたとき)

レビュー:チーム開発では、ブランチを本線に入れる前にプルリクエスト(pull request、PR。GitLabではマージリクエスト)を作ります。PRは「このブランチを main にマージしたい」という依頼で、差分・説明・コメントのやり取り・承認が1つのページにまとまります。PRの機能はGitそのものではなく、GitHubなどのサービスの機能です。

レビューで見る観点 例
目的と差分が一致しているか 説明は「表示の調整」なのに計算式が変わっていないか
正しさ(境界・0と欠損・型) 空欄を0で埋めて平均を下げていないか
再現性 自分のMacにしかない絶対パスを書いていないか
安全 パスワード・個人データ・巨大ファイルが入っていないか(秘密情報と共同作業)
確認方法 どう動作を確かめたかが書いてあるか

段階付き例題:config.txt の競合を解く

入力:main(ケンさん)に feature/threshold(ハナさん)をマージしたところ、本物のGitでは次のように表示されました。

<<<<<<< HEAD
threshold=0.6
min_rows=80
=======
threshold=0.7
min_rows=100
exclude_test_stores=true
>>>>>>> feature/threshold

ステップ1:3つの版を表にする(祖先は git show で、各版の理由は git log で確認)。

項目 祖先 HEAD(ケン) 相手(ハナ) 決定 根拠
threshold 0.5 0.6 0.7 0.6 除外で誤検知が減るので見逃し対策を優先(2人で合意)
min_rows 100 80 100(変更なし) 80 ハナさんは変えていない。ケンさんの変更を採用
exclude_test_stores なし なし true true ハナさんの追加。ケンさんは触れていない

ステップ2:ファイルを書き換える。マーカー3種を消し、決定どおりの内容にします。

threshold=0.6
min_rows=80
exclude_test_stores=true

ステップ3:add と commit。メッセージに決定の理由を書きます。

Merge feature/threshold: 閾値は0.6、テスト店舗除外を採用

閾値はケン(0.6:見逃し対策)とハナ(0.7:誤検知対策)で競合。
テスト店舗を除外すると誤検知が減るため、0.6に合意(10/9 打合せ)。

出力と意味:親が2つのマージcommitができ、2人の意図(見逃し対策・テスト店舗除外・最低行数)が残り、なぜ0.6にしたかが履歴から読めます。

Macのターミナルで試す(実Git):~/se01-practice/git-demo で、わざと競合を作って解きます。

cd ~/se01-practice/git-demo
git switch main
echo "threshold=0.5" > config.txt
git add config.txt
git commit -m "設定ファイルを追加"
git switch -c feature/threshold
echo "threshold=0.7" > config.txt
git commit -am "閾値を0.7に(誤検知対策)"
git switch main
echo "threshold=0.6" > config.txt
git commit -am "閾値を0.6に(見逃し対策)"
git merge feature/threshold
git status
cat config.txt

CONFLICT (content): Merge conflict in config.txt と表示され、cat でマーカーが見えます。ここで一度 git merge --abort を実行し、cat config.txt が threshold=0.6 に戻ることを確かめましょう。もう一度 git merge feature/threshold を実行し、今度は open -e config.txt で開いて threshold=0.6 の1行だけにして保存し、次を実行します。

grep -n -e '<<<<<<<' -e '>>>>>>>' config.txt
git add config.txt
git commit -m "Merge feature/threshold: 閾値は0.6に合意(見逃し対策を優先)"
git log --oneline --graph --all

grep -n は指定した文字列を含む行を探して表示する、読むだけのコマンドです。何も表示されなければマーカーは残っていません。

操作して理解する:競合マーカーを読み取る関数

ブラウザ内のPythonで、競合マーカー付きの文字列から自分の版・相手の版を取り出し、解決後にマーカーが残っていないかを確かめます。

conflicted = """threshold=0.5
<<<<<<< HEAD
min_rows=80
=======
min_rows=120
exclude_test_stores=true
>>>>>>> feature/threshold
log=on
"""

def split_conflict(text):
    """競合マーカーのある文字列から (自分の行, 相手の行, 共通の行) を取り出す"""
    ours, theirs, common, side = [], [], [], None
    for line in text.splitlines():
        if line.startswith('<<<<<<<'): side = 'ours'
        elif line.startswith('=======') and side == 'ours': side = 'theirs'
        elif line.startswith('>>>>>>>'): side = None
        elif side == 'ours': ours.append(line)
        elif side == 'theirs': theirs.append(line)
        else: common.append(line)
    return ours, theirs, common

def has_markers(text):
    return any(l.startswith(('<<<<<<<', '=======', '>>>>>>>')) for l in text.splitlines())

o, t, c = split_conflict(conflicted)
print('HEAD(自分):', o)
print('相手        :', t)
print('競合外      :', c)
resolved = 'threshold=0.5\nmin_rows=80\nexclude_test_stores=true\nlog=on\n'   # 話し合いで決めた内容
print('解決前にマーカーあり:', has_markers(conflicted), '/ 解決後:', has_markers(resolved))

出力:

HEAD(自分): ['min_rows=80']
相手        : ['min_rows=120', 'exclude_test_stores=true']
競合外      : ['threshold=0.5', 'log=on']
解決前にマーカーあり: True / 解決後: False

threshold=0.5 と log=on はマーカーの外なので、自動で合流済みの部分です。解決で触るのはマーカーの中だけです。

試してみよう:①resolved の文字列に ======= の行を残して、has_markers が True になることを確かめる。②競合ブロックを2つ含む文字列を作り、split_conflict が両方の中身を集めることを確かめる。

発展:競合を減らす工夫と、レビューの限界

競合を減らす工夫:①ブランチを小さく短く保ち、こまめに main を取り込む(git merge main を自分のブランチで行う)。②自動整形ツールの設定をチームでそろえ、整形だけの差分を出さない。③1つの設定ファイルに全員が書き込む構成を見直す。

マージできないファイル:Excel・画像などのバイナリは行で比べられないので、競合したら「どちらかの版を選ぶ」しかありません。Jupyter Notebook(.ipynb)は中身がJSONで、出力や実行回数まで差分になるため競合しやすく、出力を消してからcommitする、専用の差分ツールを使う、などの対策をとります。

道具:VS Code などのエディタは、競合ブロックに「Accept Current(自分)」「Accept Incoming(相手)」「Accept Both」のボタンを表示します。便利ですが、ボタンは意図を判断してくれません。押す前に3つの版と理由を確認します。

レビューの限界:レビューは人の目による確認なので、見落としはあります。数百行の差分では指摘の質が下がることが知られており、PRを小さく保つこと、テストを自動で走らせること(テストはモジュール テスト・デバッグ・品質(有料版) で扱います)と組み合わせます。

よくある失敗

失敗1:競合マーカーを残したままcommitする

誤り:エディタで片方の行を消したが、======= や >>>>>>> の行を消し忘れて add・commit した。

code = """THRESHOLD = 0.5
<<<<<<< HEAD
MIN_ROWS = 80
=======
MIN_ROWS = 120
>>>>>>> feature/threshold
"""
try:
    compile(code, 'config.py', 'exec')      # Python として読めるか確認
    print('OK')
except SyntaxError as e:
    print('SyntaxError:', e.msg, '行', e.lineno)

出力:SyntaxError: invalid syntax 行 2(Pythonの版によって文言が少し違います)。設定ファイルやコードが読めなくなり、本線を使う全員の処理が止まります。

直し方:add の前に grep -n -e '<<<<<<<' -e '>>>>>>>' ファイル でマーカーを探す。コードなら実行・テストして確かめる。模擬Gitの finish() も、マーカーが残っていると記録を拒否します。

失敗2:意図を確かめずに「全部自分の版」で解決する

誤り:競合が面倒なので、エディタの「Accept Current」を全部押した(実Gitの git checkout --ours ファイル も同じ効果)。

項目 自分の版で解決した結果 失われたもの
threshold 0.6 ―(今回は合意と一致)
exclude_test_stores 行が無い ハナさんのテスト店舗除外。誤検知が再発する

なぜ誤りか:競合ブロックの中には、相手が追加しただけの行も含まれることがあります。片方を丸ごと採用すると、それも消えます。しかも競合は「解決済み」になるので、誰も気づきません。

直し方:ブロックの中を1行ずつ表にして、祖先・自分・相手を比べる。迷ったら相手に聞く。決めた理由をcommitメッセージに書く。判断できないうちは git merge --abort で戻る。

失敗3:中身のないレビューコメント

悪いコメント 問題 良いコメント
LGTM(Looks Good To Me)だけ 何を確認したか不明 差分の3ファイルを確認。手元で sample.csv を集計し、件数が説明どおり 1,203→1,215 になることを確認しました。承認します
ここおかしい どこが、なぜ、どうすれば clean.py 12行目:fillna(0) だと未入力の金額が0円として平均に入ります。欠損は除外するか、件数を別に数えませんか
全部書き直して 相手が動けない 必須:絶対パス /Users/ken/... を相対パスに。任意:変数名 df2 を orders_paid にするとわかりやすいです

直し方:①場所(ファイル・行)、②根拠(何が起きるか)、③提案(どう直すか)を書き、必須か任意かを区別する。人ではなくコードについて書く。承認するときも、何を確かめたかを書く。

まとめ

用語

競合(コンフリクト)(merge conflict)

マージで両方のブランチが同じ箇所を別々に変更していて、Gitが自動で結果を決められない状態。

競合マーカー(conflict markers)

競合箇所に書き込まれる <<<<<<<、=======、>>>>>>> の行。自分と相手の版を区切る。

プルリクエスト(pull request (PR))

ブランチを本線にマージしたいという依頼。差分・説明・レビュー・承認をサービス上で扱う。

レビュー(code review)

変更を本線に入れる前に、他の人が差分を読んで正しさ・安全・読みやすさを確認すること。

よくある質問

競合が起きるのは、誰かが間違えたからですか?

いいえ。同じ場所を別々の理由で変えれば、正しい作業同士でも競合します。競合は意図の衝突を見える化する仕組みです。ブランチを小さく保ち、こまめに本線を取り込むと頻度は減ります。

自分の版と相手の版、どちらを優先すべきですか?

決まりはありません。両方の理由(commitメッセージ、PRの説明、本人への確認)を読み、目的に合う内容を決めます。片方を丸ごと採用すると、相手の追加行まで消えることがある点に注意してください。

git merge --abort と git restore の違いは?

git merge --abort は進行中のマージを中止して、マージ前の状態に戻します。git restore ファイル は作業ツリーの変更を捨てる操作で、マージの中止ではありません。競合中に迷ったら --abort を使います。

どこで失敗しやすいですか?

①マーカーの消し忘れ、②『Accept Current』の一括押しで相手の変更を消す、③解決後にテストしない、④理由を書かずにcommitする、の4つです。解決後は grep でマーカーを探し、動作を確かめてからcommitします。

条件が変わって、競合したのがExcelファイルだったら?

Excelはバイナリなので行の比較ができず、マーカーも書き込まれません。どちらかの版を選ぶしかないため、相手と相談して選び、必要なら手で統合した版を作ります。表データはCSVなどのテキストで管理すると競合を扱いやすくなります。

仕事では、レビューで何をどこまで見ればよいですか?

まず『説明と差分が一致しているか』、次に正しさ(境界・0と欠損・型)、再現性(パス・環境)、安全(秘密情報・個人データ・巨大ファイル)、最後に読みやすさです。重大な問題を必須、好みを任意として区別し、確認した内容を書いて承認します。

確認問題

問1 次の競合ブロックで、======= より下、>>>>>>> feature/x より上にある行は何か。

  1. ① 取り込もうとしているブランチ(feature/x)の版
  2. ② 今いるブランチ(HEAD)の版
  3. ③ 共通の祖先の版
  4. ④ Gitが自動で決めた最終版
解答と解説

正解:①

<<<<<<< HEAD 〜 ======= が自分の版、======= 〜 >>>>>>> 名前 が相手の版です。

問2 競合を解決した後、commitの前に必ず確かめるべきことはどれか。

  1. ① ファイルの更新日時
  2. ② コミットIDの桁数
  3. ③ ブランチ名の長さ
  4. ④ 競合マーカー3種がすべて消え、内容が両方の意図に沿っていること
解答と解説

正解:④

マーカーが残るとファイルが壊れ、意図の確認を怠ると相手の変更が黙って消えます。

問3 競合の解決方法がわからず、いったんやめたい。最も安全な操作はどれか。

  1. ① フォルダをゴミ箱に入れる
  2. ② git merge --abort
  3. ③ マーカーを残したままcommitする
  4. ④ 競合したファイルを削除してcommitする
解答と解説

正解:②

merge --abort はマージを始める前の状態に戻します(未commitの変更が無い状態で始めた場合)。

問4 マージで競合が出なかった。正しい理解はどれか。

  1. ① Gitが行の位置で衝突を検出しなかっただけで、意味的な矛盾はテストやレビューで確かめる必要がある
  2. ② 変更の意味としても必ず正しい
  3. ③ レビューは不要になる
  4. ④ テストは不要になる
解答と解説

正解:①

Gitは意味を見ていません。離れた行の変更が意味的に矛盾していても自動で合流します。

問5 レビューコメントとして最も役に立つものはどれか。

  1. ① LGTM
  2. ② ここおかしい
  3. ③ 全体的にいまいち
  4. ④ clean.py 12行目:fillna(0) だと未入力の金額が平均に0円として入ります。欠損は除外しませんか(必須)
解答と解説

正解:④

場所・何が起きるか・提案・必須かどうかがそろっていて、作者がすぐ対応できます。

問6 プルリクエスト(PR)について正しい説明はどれか。

  1. ① Git本体のコマンドで、ローカルだけで完結する
  2. ② commitを削除するための機能
  3. ③ ブランチを本線にマージしたいという依頼で、差分・説明・レビュー・承認をGitHubなどのサービス上でまとめて扱う
  4. ④ 競合を自動で解決する機能
解答と解説

正解:③

PRはGitHub・GitLabなどのサービスの機能です。マージ前のレビューの場になります。

次に学ぶこと

次は 秘密情報と共同作業 で、レビューで見つけたAPIキーのような秘密情報や、個人データ・巨大データをリポジトリに入れない方法と、リモートリポジトリを使った共同作業の流れを学びます。

3方向マージの判定に不安があれば branchとmerge に戻りましょう。

キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。

アプリを無料で開く有料版と無料版の違い