この記事はNuco Advent Calendar 2024の10日目の記事です。
1.はじめに
チーム開発でgitを使用していれば必ず遭遇するコンフリクト。適切に解消しなければ思わぬバグや機能の欠落などを生み、チームに混乱をもたらす可能性があります。
この記事では、コンフリクトについての深い理解と適切な解消やコンフリクト回避をわかりやすくまとめました。
初学者の勉強や、新人研修にぜひご活用頂けたら幸いです。
弊社Nucoでは、他にも様々なお役立ち記事を公開しています。
よかったら、Organizationのページも覗いてみてください。
また、Nucoでは一緒に働く仲間も募集しています!興味をお持ちいただける方は、こちらまで。
2.Conflictとは
gitはmergeする際、変更箇所を自動的に統合してくれますが、同じファイルの同じ場所への変更が2つのブランチにそれぞれある場合、どちらを適用すればいいかまではgitは判断できず、手動で解決する必要があります。これがコンフリクトです。
コマンドでmergeした時にコンフリクトを起こした場合は、gitは下記のようなメッセージでコンフリクトの解消を求めます。
vs codeであれば下記写真のようにコンフリクト箇所を表示するので、コンフリクト解消のため修正が必要です。具体的な修正方法は4章をご確認ください。
また、git statusコマンドを使用すれば、どのファイルでコンフリクトが起きているか確認できます。詳しくは4章をご確認ください。
3.なぜConflictが起きるのか
コンフリクトが発生する理由は大きくいうと、merge、cherry-pick、rebase、pull 時に「同じ場所を異なる方法で変更した」ことに起因します。
では具体的にどのような時にコンフリクトが起こるのか、例をひとつずつ見ていきましょう。
例1. 同じ行の変更
発生条件: 複数のブランチで同じファイルの同じ行を異なる内容で変更した場合。
例: feature-branchで abc.py の2行目を変更し、mainでも同じ行を変更した場合。
# mainブランチ
def calc(x, y):
result = x + y
return result
# featureブランチ
def calc(x, y):
result = x * y
return result
例2. 同じファイルの変更
発生条件: 同じファイルが異なるブランチで変更された場合でも、変更箇所が異なればGitは自動でマージできます。ただし、変更の内容に依存関係があったり、同じ部分で変更が重なる場合に競合が発生します。
例: feature-branchで abc.py の関数calcを変更し、mainでも関数calcに関する修正があった場合。
# mainブランチ
def calc(x, y):
result = x + y
print("Main branch: Adding x and y:", result)
return result
# featureブランチ
def calc(x, y):
result = x + y
print("Feature branch: Adding x and y:", result)
result_squared = result ** 2
print("Squared result:", result_squared)
return result
↓コンフリクト発生
def calc(x, y):
<<<<<<< HEAD
result = x + y
print("Main branch: Adding x and y:", result)
return result
=======
result = x + y
print("Feature branch: Adding x and y:", result)
result_squared = result ** 2
print("Squared result:", result_squared)
return result
>>>>>>> feature-branch
例3. ファイル削除と変更の競合
発生条件: 片方のブランチでファイルを削除したのに、他のブランチでそのファイルに変更を加えた場合。
例: feature-branchで abc.py を削除し、mainで abc.py に変更を加えた場合。
例4. ファイル追加と削除の競合
発生条件: 一方のブランチでファイルを追加し、もう一方でそのファイルを削除した場合。
例: feature-branchで abc.py を新しく追加し、mainで abc.py を削除した場合。
例5. 同じファイルを移動または名前変更
発生条件: 同じファイルが異なるブランチで移動または名前を変更され、両方の変更がマージされるとコンフリクトが発生します。
例: feature-branchで abc.py をdef.py にリネームし、mainで abc.py を別のフォルダに移動した場合。
例6. ブランチ間の履歴の違い
発生条件: 複数のブランチで独立して変更が進んだ場合、特に異なる開発者が同時に別々の変更を行うと履歴にギャップが生まれることがあります。これが後で統合しようとするときにコンフリクトを引き起こすことがあります。
1.最初の状態(共通の親コミット)
最初、mainブランチがあり、開発者Aと開発者Bが別々の変更を加え始めます。
◆ commit M (main) ← 初期状態、親コミット |
---|
2. 開発者Aの変更(feature-Aブランチ)
開発者Aは、feature-Aブランチで作業を開始します。
◆ commit M (main) ↓ ◆ commit A1 (feature-A) ← 開発者Aの変更 |
---|
3. 開発者Bの変更(feature-Bブランチ)
一方、開発者Bもfeature-Bブランチで別の変更を加えます。
◆ commit M (main) ↓ ◆ commit B1 (feature-B) ← 開発者Bの変更 |
---|
4. 統合しようとしたとき
この時点で、feature-Aとfeature-Bブランチには異なる変更が含まれており、mainブランチにはそれらが統合されていません。この状態で、例えば、先にfeature-Aがmainにマージし、feature-Bが後からmainにマージしようとした場合、feature-Aの変更が反映されていないため、履歴にギャップが生じます。
◆ commit M (main) ↓ ◆ commit A1 (feature-A) ← Aの変更が先にマージされた ↓ ◆ commit B1 (feature-B) ← Bの変更を後から統合しようとすると..... |
---|
ここで問題が生じます。もし、A1とB1が同じファイルやコードの行を変更している場合、Gitはコンフリクトを報告します。
5. コンフリクト発生
異なるブランチで並行して作業すると、それぞれのブランチで進んだ変更が元々のコードに対して「独立して」加わるため、履歴にギャップが生じます。
◆ commit M (main) ↓ ◆ commit A1 (feature-A) ↓ ↘ ↓ ◆(コンフリクト) ↓ ↘ ↓ commit B1 (feature-B) |
---|
例7. マージの順序による競合
発生条件: feature-branchを先にmainにマージし、その後で逆にmainをfeature-branchにマージした場合、両者の変更が衝突することがあります。
4.Conflictを解消するためには
基本編
先ほど3の例2で紹介したコンフリクトで見ていきましょう。
mainブランチにいて、featureブランチをmergeするために下記コードを実行
git merge feature
コンフリクトが発生したことを確認する
コンフリクトが発生し、Git は以下の「自動マージ失敗;コンフリクトを修正し、修正をコミットして」とメッセージを表示します。
Automatic merge failed; fix conflicts and then commit the result.
コンフリクトが発生しているファイルを確認する
コンフリクトが発生したファイルは、下記コマンドで確認できます。
git status
コンフリクトが発生しているファイルは次のように表示されます。
コメント部分は説明文です。
On branch main #現在いるブランチ名
You have unmerged paths. #コンフリクトにより、mergeできてないよ
(fix conflicts and run "git commit") #コンフリクト解消してコミットしてね
(use "git merge --abort" to abort the merge) #mergeを中止の場合 git merge --abortコマンドしてね
Unmerged paths: #解決されていないファイルリストは下記だよ
(use "git add <file>..." to mark resolution) #完了したらaddしてね
both modified: abc.py #abc.pyでコンフリクト起きてるよ
no changes added to commit (use "git add" and/or "git commit -a")
#まだ解決済みのファイルがステージ(インデックス)されていないよ
※コンフリクトが複数ファイルある場合、both modifiedのところのファイル名が複数行になります。
コンフリクト部分を手動で解消する
コンフリクトが発生したファイルを開くと、次のような形式でマーカーが表示されます。
<<<<<<< HEAD
# mainブランチ
def calc(x, y):
result = x + y
print("Main branch: Adding x and y:", result)
return result
=======
# featureブランチ
def calc(x, y):
result = x + y
print("Feature branch: Adding x and y:", result)
result_squared = result ** 2
print("Squared result:", result_squared)
return result_squared
>>>>>>> feature
<<<<<<< HEAD から ======= までは現在のmainブランチ。
======= から >>>>>>> feature までは、マージ元のfeatureブランチです。
ここで、どの変更を採用するか、あるいは両方を組み合わせるかを手動で判断して修正します。
修正後、マーカー部分(<<<<<<<, =======, >>>>>>>)を削除します。
※手作業ですので、消さなくていいところや、マーカー部分の削除漏れに注意してください。
選択的にマージ(git checkout --theirs / git checkout --ours)もできます。
コンフリクトを解消する際に、どちらかの変更を選んで完全に採用する方法です。以下のようにコマンドで選択できます。
--ours: 現在のブランチの変更を選択してコンフリクトを解消する
git checkout --ours 修正しているファイル名
--theirs: もう一方のブランチの変更を選択してコンフリクトを解消する
git checkout --theirs 修正しているファイル名
この方法では、手動で両方の変更を確認して選ぶことなく、簡単に一方の変更を採用することができます。
コンフリクト解消後のファイルをステージする
コンフリクトを解消したファイルをステージに追加するには、次のコマンドを使います。
git add abc.py(コンフリクト解消したファイル名)
コンフリクト解消後にgit statusでコンフリクト未解消のファイルがないか確認
git status
【実行結果】
※addした後、コンフリクト解消されたことを示しています。
↓もしaddしていない場合、、、
addが実行されてないので、addするようにgitが示しています。
コンフリクト解消のコミットを作成する
コンフリクトを解消した後、通常のコミットを作成します。
git commit
Git は自動的にコンフリクト解消に関するメッセージを用意しますが、必要に応じて自分でメッセージを編集しても構いませんし、git commit -m "メッセージ"
でも構いません。
変更が適用されていること、または他のブランチとの統合がうまくいったことを確認
git log
【結果】
mergeが上手く完了しました。
応用編
コンフリクト解消に活用できるコマンドを3つご紹介します。
【1つ目】共通の祖先コミットを探す
↓このコマンドは二つのブランチの共通の先祖コミット(共通の基底コミット)を探すためのコマンドです。
git merge-base ブランチ名1 ブランチ名2
merge-base は、指定された2つのブランチの共通の祖先(または祖先コミット)を見つけて、そのコミットIDを出力します。たとえば、featureと feature-1 のブランチがそれぞれ異なるコミットを持っている場合、その2つのブランチが最後に分岐した地点(共通の祖先)を特定します。
【2つ目】共通祖先からのブランチ間の差分を確認
↓git merge-base を使って、2つのブランチ間の差分を見たい場合、下記のように git diff と組み合わせて使うことができます。これにより、2つのブランチが共通の祖先からどのように変更されたのかを視覚的に確認できます。
git diff $(git merge-base ブランチ名1 ブランチ名2)..ブランチ名2
【結果】
feature-a ブランチと feature-b ブランチの共通の祖先(マージベース)から、feature-b ブランチの現在の状態までの差分を表示しています。
feature-bでは共通の祖先からの変更は足し算を引き算にしています。
【3つ目】共通の祖先からのログ
↓共通の祖先を調べた後、その祖先からどのようにコミットが進んでいるのかを確認するために git log を使うことができます。
git log --oneline $(git merge-base ブランチ名1 ブランチ名2)..ブランチ名2
【結果】
feature-bブランチの祖先はmainブランチで、feature-bを作成後、2回コミットしたので、それらのコミットIDが表示されました。
5.Conflict解消時の注意点
コンフリクト解消は、チーム全体の作業に影響を与える可能性があります。
そのため、コンフリクトが起きている箇所の修正方法が不明な時は、必ずチームメンバーに確認してから解消するようにしましょう。
適当になんとなくで解消するのが一番よくないです。
コンフリクトがマークされているコードだけを確認するのではなく、全体をみて、コンフリクトの原因を把握する必要があります。コンフリクトが起きている箇所をどちらかのブランチの内容に合わせればいいわけじゃない時も多くあります。
例えば、Masterブランチから最新のコミット1から作成したfeatureAブランチで作業したあと、Masterブランチへmergeするときに、featureBブランチがコミットBをmergeしたあとだった場合、コミット1からコミットBへの変化点をきちんと理解した上で修正が必要です。
そうでなければ、featureBで実装した今後も必要である機能が失われてしまう可能性があります。
コンフリクトの箇所が多すぎる場合は、マージを中止して別の方法で対処することも検討しましょう。消さなくていいところまで消してしまうなど、ヒューマンエラーが発生する可能性が大きくなります。
コンフリクト解消後は必ず動作確認はお忘れなく。
6.Conflictさせないためには
こまめにpullをする
こまめに(git pull)して、リモートリポジトリの最新状態をローカルに取り込んでから作業をすることを習慣づけます。これにより大きな変更を一度に取り込むのではなく、段階的に変更を取り込むことができ、コンフリクトの可能性を減らします。また、自分の作業が進んでからプッシュする前に、リモートで他の人が変更を加えていないかを確認しましょう。
※pullするときは、pullするブランチと現在のブランチがイコールであることを必ず確認して下さい。
例えばmainブランチにいて、mainをpullしようと思ったのに、git pull origin feature
としてしまったらfeatureブランチの変更がmainにmergeされてしまいます。
コミュニケーションを密にする
チームメンバーとのコミュニケーションを活発にし、誰がどの部分を担当しているかを共有しておくことも重要です。重複した作業や同じファイルの同時変更を避けるためです。
複数人で同じファイルを変更しない
同じファイルに対して、複数のブランチでの並行作業が起こらないようにする。作業の順番や、作業ファイルの分担をするなどの対策を行い管理する。
コンフリクトが起きやすいファイルを分ける
特に頻繁に変更が加えられる可能性のあるファイルをモジュール化して別のファイルに分けると、コンフリクトのリスクを低減できます。
変更範囲を小さく保つ
大規模な変更を一度に行うとコンフリクトが発生しやすくなるため、変更を小さく、段階的に行うことが重要です。例えば、一度に多くのファイルを変更するのではなく、機能ごとに小さな単位でコミットしていきます。
7.Conflict対策の役立つVSCode拡張機能
GitLens
GitLensは、コードの履歴や変更履歴、変更者、コミットメッセージ、差分の表示など、Gitに関する情報を簡単に取得することが可能です。
コードのブレーム情報表示
行ごとのコミット履歴や、誰がいつ変更したかをコードの上に表示できます。
確認したい行をクリックすると、変更者、いつ変更したのか、コミットメッセージが表示されます。
リポジトリビュー
コミット、ブランチ、タグなどの情報をサイドバーから簡単に閲覧できます。
Git History
Git Historyは、Gitリポジトリの履歴を視覚的に管理・閲覧するためのツールです。リポジトリ内のコミット履歴や変更内容を簡単に確認でき、コミットの詳細やブランチの履歴、タグ、差分などを素早く把握可能です。
対象のファイルを右クリックしたら、「Git:View File History」を選択します。
コミット履歴の表示
Gitリポジトリ内の全コミット履歴を簡単に閲覧できます。
コミットごとに、メッセージ、著者、日付などの詳細を表示します。
コミットの差分確認
コミット間の差分を視覚的に比較できます。これにより、変更内容を直感的に把握できます。黄色ハイライトのところをクリックします。
workspaceだと現在の作業との差分でpreviousだと選択したコミットとその一つ前のコミットの差分です。
履歴のフィルタリング
コミット履歴を日付やブランチ、タグなどで絞り込んで表示できるため、必要な情報に素早くアクセスできます。
Git Graph
Git Graphは、Gitリポジトリのブランチ構造を視覚的に表示し、Git操作を効率的に行えるツールです。これにより、複雑なコミット履歴やブランチの状態を直感的に把握し、Git操作(マージ、リベース、コミットなど)を視覚的に実行可能です。
インストール後のgit Graphの場所は下記黄色ハイライトです。
ブランチの視覚化
Gitリポジトリ内のブランチの履歴や関係性をツリー状に表示します。これにより、複数のブランチ間の変更がどのように統合されたのかを簡単に確認できます。
差分の確認
任意のコミット間で変更されたファイルやその内容を差分ビューとして表示できます。
コードの変更点を視覚的に確認することで、レビューやデバッグがしやすくなります。
黄色ハイライトのところをクリックすると、差分が確認できます。
Git操作が視覚的にできる
リモートリポジトリのブランチも視覚的に表示され、cherry-pickやrevertなどの操作をGit Graphから直接行えます。
フィルタリングと検索
コミット履歴をフィルタリングしたり、特定のコミットを検索して表示できます。
コミットメッセージや変更内容を基に履歴を絞り込むことができます。
8.実践編 試しにConflictさせて解消してみよう
ローカルver
①mainブランチに下記コードをコミットしてpushします。
def calc(x, y):
result = x + y
result_squared = result ** 2
return result_squared
a = calc(1, 3)
print(a)
②mainを元にfeature-1ブランチを作成しましょう
git checkout -b feature-1
③下記に書き換えたコードでコミットしてpushしましょう。
def calc(x, y, z):
intermediate_value = x + y * z
final_result = intermediate_value ** 2
return final_result
total = calc(1, 3, 5)
print(total)
④mainにcheckoutして、mainも2行目と3行目を変更し、コミットしてpushします。
def calc(x, y):
intermediate_value = x + y * 10
final_result = intermediate_value ** 3
return final_result
total = calc(1, 3)
print(total)
⑤mainブランチでgit merge feature-1を実行するとコンフリクトが起きます。
<<<<<<< HEAD
def calc(x, y):
intermediate_value = x + y * 10
final_result = intermediate_value ** 3
=======
def calc(x, y, z):
intermediate_value = x + y * z
final_result = intermediate_value ** 2
>>>>>>> feature-1
return final_result
total = calc(1, 3, 5)
print(total)
⑥git status
でどのファイルがコンフリクトが起きているか確認します。
今回は1ファイルしかないですが、複数のファイルでコンフリクトが起きることもあります。漏れのないようにgit statusで確認するようにしましょう。git statusの見方はコンフリクト解消方法の章をご参考ください。
⑦コンフリクトが起きているabc.pyののファイルを開いて修正します。
もしmainを正にしたければ
=======から>>>>>>> feature-1を含む行を削除して、<<<<<<< HEADを削除します。
もしfeature-1が正にしたければ
<<<<<<< HEADから=======を含む行を削除して、>>>>>>> feature-1を削除します。
ただどちらか一方ではなく、両方の変更が必要な場合もあります
たとえば、ベースはfature-1だけど、final_resultは三乗にする必要があるときは
feature-1に合わせて、final_result を三乗に書き換えます。
def calc(x, y, z):
intermediate_value = x + y * z
final_result = intermediate_value ** 3
return final_result
total = calc(1, 3, 5)
print(total)
⑧再度git status
で解消もれのファイルがないか確認します。
⑨ステージングして、コミットし、プッシュして完了です。
git add .
git commit
かgit commit -m"コメント"
git push origin main
プルリクver
①から④の工程は上と同じです。
⑤git hubで新規プルリクエストを作成した際に、コンフリクトで自動mergeできないときは、下記写真のように黄色ハイライトのメッセージが出ますが、一旦create pull requestで進みます。
すると下記が表示されますので、Resolve conflictsをクリックします。
下記のようにコンフリクト箇所が表示されます。
ローカルverでやったように必要な箇所を残して、不要な箇所は削除して mark as resolvedをクリックし、commit mergeボタンが表示されるのでクリックして完了です。
あとはmerge pull requestボタンをクリックして、だれかにmergeコンファームをしてもらったら完了です。
9.おまけ(gitコマンド一覧表)
慣れてくると自然と覚えてくるのかもしれませんが、慣れないうちはあのコマンドなんだっけ?ということが多いのでコマンド一覧作成しました。
弊社Nucoでは、他にも様々なお役立ち記事を公開しています。
よかったら、Organizationのページも覗いてみてください。
また、Nucoでは一緒に働く仲間も募集しています!興味をお持ちいただける方は、こちらまで。