キーワードで検索

今日を知り、明日を変えるシステム運用メディア

インフラエンジニアの為のGit問題解決

インフラエンジニアの為のGit問題解決

この記事は、インフラエンジニアのためのGitシリーズの第3弾です。以前作成した記事「インフラエンジニアの為のGit実践」では、Git操作のキャンセルや削除について記載しました。

この記事には、Gitで起こるコンフリクトや競合といった問題を解決する方法やそのメカニズムに関する図や文章を掲載します。

コンフリクトの解消方法

Gitにおいてのコンフリクトとは、Mergeする際にMerge元とMerge先で同一のオブジェクト(ファイル)に異なる変更内容がある状態を指します。

コンフリクトは、ローカル環境でMergeを行う際にも発生する可能性があり、リモートリポジトリ上でMergeを行う際も同様に発生する可能性があります。

pushやpullを行う際にも、コンフリクトと呼べる事象が発生することがありますが、この章では解説を省略します。

ローカル環境でコンフリクトを解消する

ローカル環境において、分岐したコミットをMergeしようとする際、それぞれのコミットで同一のオブジェクトが更新されている場合、コンフリクトが発生します。

例えば、ローカル環境にて`test`ブランチに`main`ブランチの状態を取り込むためにMergeしようとする場合、同一のオブジェクト(以下の例では`h.txt`)に異なる変更内容があると、以下のようにコンフリクトに関するエラーが表示されます。

$ git merge main
Auto-merging h.txt
CONFLICT (content): Merge conflict in h.txt
Automatic merge failed; fix conflicts and then commit the result.

この際、テキストエディタやコードエディタを開くと、以下のように、Merge元とMerge先のファイル内容が表示されています。

この画面のコードやGitが付与したMerge元とMerge先を示す文字列を削除、修正してそれぞれの変更内容を統合できますが、ここではコードエディタの機能を利用して解決することとします。コードエディタとして、Visual Studio Codeを用いた例を示します。

まずは、画面上にある「マージエディターで解決」ボタンをクリックします。

ローカル環境でコンフリクトを解消する1

Merge元とMerge先それぞれのファイルの状態が画面上部に表示され、画面下部には統合予定のファイル内容が表示されています。

統合予定のファイル内容には、Merge元とMerge先のどちらの情報も反映されていないので、マージエディターの機能を利用して反映することにします。

まずは、Merge元である`main`ブランチの情報を反映するために、以下キャプチャ赤枠部分のボタンをクリックします。

ローカル環境でコンフリクトを解消する2

そうすると、画面下部に`main`ブランチのファイルの内容が反映されます。

マージ先である`test`ブランチのファイルの内容も反映したいので、もう一方のボタン(キャプチャ赤枠部分)をクリックします。

ローカル環境でコンフリクトを解消する3

画面下部に`main`ブランチと`test`ブランチの両方のファイルの最新状態が表示されました。

今回は、それぞれのファイルの内容を2行にして残すのではなく、1行にまとめたいので、手動で編集することにします。

ローカル環境でコンフリクトを解消する4

このように、1行に統合するために手動で修正しました。

統合後の内容が画面下部の状態でよければ、「マージの完了」ボタンをクリックします。

ローカル環境でコンフリクトを解消する5

マージ用の画面が閉じ、最終的なファイルの状態が表示されています。さきほど統合した内容は自動コミットはされず、ステージされている状態になっています。

ローカル環境でコンフリクトを解消する6

`git status`にて状態を確認しても同様です。

$ git status
On branch test
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   h.txt

ログにもMergeコミットは残っていません。

$ git log --graph
* commit e7f6fdfeb30b65050cf5c850564f5179040e9b8d (HEAD -> test)
| Author: testuser-ab12 <git.test@xxxxxxx.net>
| Date:   Sun Feb 23 09:07:52 2025 +0900
| 
|     test-user test 09:07:52
|   

コンフリクトを解消した後の状態をコミットします。

$ git commit -m "test-user test Conflict Resolution 09:14:32"
[test e126ca4] test-user test Conflict Resolution 09:14:32

ログを確認すると、`main`ブランチのコミット履歴が取り込まれ、最後に先ほどコミットした内容(`test-user test Conflict Resolution 09:14:32`)も表示されています。

これで、コンフリクトを解消し、`main`ブランチの取り込み(Merge)が完了しました。

$ git log --graph
*   commit e126ca4a7dcfb1e7bce70520c9c46a253715095b (HEAD -> test)
|\  Merge: e7f6fdf 9235fea
| | Author: testuser-ab12 <git.test@xxxxxxx.net>
| | Date:   Sun Feb 23 09:14:33 2025 +0900
| | 
| |     test-user test Conflict Resolution 09:14:32
| |   
| *   commit 9235fea3b746db3f05bca99ab9f3507a1a4748a8 (origin/main, origin/HEAD, main)
| |\  Merge: 1b7cfe2 2952bd2
| | | Author: KaXXXXXXXoi <50761722+kaXXXXXXXoi2@users.noreply.github.com>
| | | Date:   Sun Feb 23 09:00:03 2025 +0900
| | | 
| | |     Merge pull request #11 from kaXXXXXXXoi2org/test
| | |     
| | |     main-user test 08:59:23
| | | 
| | * commit 2952bd200d9397255471ca1e0697d63075bd1f06 (origin/test)
| |/  Author: kaXXXXXXXoi2 <kdoi@xxxxxxx.net>
| |   Date:   Sun Feb 23 08:59:23 2025 +0900
| |   
| |       main-user test 08:59:23
| |   
| *   commit 1b7cfe2581e320038495d94a75e88c8af1aa8cf0
| |\  Merge: bea9094 82fd575
| | | Author: testuser-ab12 <git.test@xxxxxxx.net>
| | | Date:   Sun Feb 23 08:21:26 2025 +0900
| | | 
| | |     Merge pull request #10 from kaXXXXXXXoi2org/test
| | |     
| | |     test-user test 08:04:20
| | | 
* | | commit e7f6fdfeb30b65050cf5c850564f5179040e9b8d
| |/  Author: testuser-ab12 <git.test@xxxxxxx.net>
|/|   Date:   Sun Feb 23 09:07:52 2025 +0900
| |   
| |       test-user test 09:07:52
| | 

リモートリポジトリでコンフリクトを解消する

リモートリポジトリでコンフリクトが発生した場合も、ローカル環境でコンフリクトを解消する方法と同様の手順を実施します。

GitHubにて、`main`ブランチに`test`ブランチの内容を取り込む際の例で説明します。

Pull request作成後の画面にて、`b.txt`がコンフリクトしている旨が表示されています。

コンフリクトを解決するために「Resolve conflicts」ボタンをクリックします。

リモートリポジトリでコンフリクトを解消する1

すると、ローカル環境のエディタにてコンフリクトしたファイルが表示された際と同様の画面が表示されました。

VSCodeのようにマージエディター機能は存在しないようなので、手動にて統合後のファイル状態に修正します。

リモートリポジトリでコンフリクトを解消する2

Merge元とMerge先それぞれのファイルを統合した状態に修正後、「Mark as resolved」ボタンをクリックします。

リモートリポジトリでコンフリクトを解消する3

「Commit merge」ボタンをクリックし、先ほど統合のために修正した内容をコミットします。

リモートリポジトリでコンフリクトを解消する4

コンフリクトが解消し、マージできる状態になりました。

このまま「Merge pull request」ボタンにて、マージを完了させることもできるようですが、「Merge branch ‘main’ into test」の部分をクリックすると、先ほど作成したコンフリクト解消用のコミットを確認できます。

リモートリポジトリでコンフリクトを解消する5

コンフリクト解消用のコミットの変更差分を以下のように確認できます。

Merge先(`test`ブランチ)には、`aaa222`という文字列のみがファイルに保存されていましたが、コンフリクト解消時に`main`ブランチのオリジナルの内容である`111`の部分を統合したので、このように差分が表示されています。

リモートリポジトリでコンフリクトを解消する6

Mergeを実行すると、以下のようにMerge完了の画面が表示されました。

これで、コンフリクトを解消し、`test`ブランチの取り込み(Merge)が完了しました。

リモートリポジトリでコンフリクトを解消する7

コンフリクトのパターン

「コンフリクトの解消方法」にて解説した

  • ローカルでのコンフリクト
  • リモートリポジトリでのコンフリクト

について、コンフリクト発生時の状態を図解します。

ローカルでのコンフリクト

「ローカル環境でコンフリクトを解消する」の章で例示したコンフリクトは、以下のような状態になっていました。

ローカルでのコンフリクト

このように、ローカルリポジトリ上の2つのブランチをMergeしようとする時に、分岐したコミットにて同じオブジェクト(ファイル)がそれぞれ更新され、異なる内容になっている状態でMergeを行うと、コンフリクトが発生します。

「ローカル環境でコンフリクトを解消する」の章では、`git merge`コマンドの実行時にコンフリクトが発生しましたが、`git pull`を行う際もコンフリクトが発生することがあります。

`git pull`を行うと、`git fetch`(リモートリポジトリからローカルリポジトリのリモート追跡ブランチ(origin/mainなど)に情報を取り込む処理)と、`git merge`(リモート追跡ブランチの情報をローカルブランチに取り込む処理)の2つ処理が実行されるためです。

つまり、Mergeを行う時にコンフリクトが発生することがあると考えることができます。

なお、Pullの場合、取り込み元と取り込み先にて同一のファイルを更新していなくてもエラーが出ることがあります。それについては、後の章に記載します。

リモートリポジトリでのコンフリクト

Merge対象に同じファイルの更新があれば、ローカルリポジトリの場合と同じくコンフリクトが発生します。

「リモートリポジトリでコンフリクトを解消する」の章にて例示したコンフリクトは、以下の状態により発生しました。

リモートでのコンフリクト

2つのブランチをMergeしようとする時に、分岐したコミットにて同じオブジェクト(ファイル)がそれぞれ更新され、異なる内容になっている状態でMergeを行うと、コンフリクトが発生することは、ローカルリポジトリの場合と同じです。

push時の競合

ここまで異なる内容の同じオブジェクト(ファイル)をMergeする際に発生するコンフリクトについて解説しましたが、ここからはそれ以外の競合について解説します。

コンフリクトを翻訳すると、競合とも呼べるかもしれませんが、ここでの競合は、更新されるファイルが同じであっても異なっていても発生しうる、push時の競合について解説します。

push時の競合は、主に同じブランチを複数人で共同編集するときに発生します。また、開発メンバーが自分一人の場合でも、手元でresetなどしていたら発生します。

push時の競合が起きる原因は、pushしようとしているコミット履歴が、push先より古いためです。

git_push時の競合
(この図を※1とします)

例えば、上の図のようにリモートリポジトリでは「コミットB」を親とする「コミットC」に`main`ブランチのポインタが指されていて、pushをしようとするローカルリポジトリでは「コミットB」を親とする「コミットD」に`main`ブランチのポインタが指されている状態があったとします。

この状態で`git push`を行うと、以下のように、リモートリポジトリ側に存在しない作業(コミットD)がローカルリポジトリに存在する旨が指摘され、pushが完了しません。

$ git push origin test
To github.com:kaXXXXXXXoi2org/git_test.git
 ! [rejected]        test -> test (fetch first)
error: failed to push some refs to 'github.com:kaXXXXXXXoi2org/git_test.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

適切な対処

上記のエラーに示されているように、`git pull`を行うことがこの場合の適切な対処です。

`git pull`を行うと、以下のように、`git fetch`と`git merge`の2つの処理が実行されます。

git_pullの説明

上記の例の場合、特に問題はありませんが、※1の状態からpullを行うと競合が発生します。

※1の状態からpullを行うと、Fetch処理は成功しますが、Merge処理において以下のように、ブランチが分岐しているのでMergeの方法を選択する必要がある旨が表示され、Mergeが完了しません。

$ git pull origin test
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 248 bytes | 35.00 KiB/s, done.
From github.com:kaXXXXXXXoi2org/git_test
 * branch            test       -> FETCH_HEAD
   1c0c061..f4999dc  test       -> origin/test
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint: 
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint: 
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

選択できるMergeの方法は以下の3種類があります。

方法オプション文字列概要特徴
マージ–no-rebaseMerge元(取り込み元)の情報をMerge先(取り込み先)にMergeするためのコミットを作成する。
  • Merge用の新たなコミットが作成される
  • Merge前のコミット履歴は残る
リベース–rebaseMerge元(取り込み元)のコミットの後にMerge先(取り込み先)のコミットを連結させる(Merge先のもともとのコミットはコミット履歴から消える)
  • Merge用コミットは作成されない
  • Merge先のもともとのコミットはなかったことになる
  • コミット履歴が1直線になる
ファストフォワード–ff-onlyMerge元(取り込み元)のコミット履歴に、Merge先(取り込み先)のコミットが含まれる場合のみ使用可能。Merge先(取り込み先)のブランチのHEADがMerge元(取り込み元)のブランチのHEADに追いつくだけの動作。git pull時はまずこのオプションにて処理が試行される。
  • オプション利用のための条件がある
  • このオプションが利用できる場合、自動的にこのオプションが採用されるため、意図的に使用するケースは少ないと言える

ファストフォワードが選択できる場合、そもそも`git pull`を実行した時にエラーは発生しないため、今回のようなケースにおいては、マージまたはリベースのどちらかを選択することになります。

マージとリベースの違いは、マージコミットを残すか残さないかの違いです。リベースの方がマージコミットがない分、きれいな履歴になりますが、リベース時にリベースをしようとする側が作成した複数のコミットに対して、それぞれコンフリクトの判断が必要です。その分、コンフリクトに対する対応が複雑になる可能性があるため、チームのGit運用方針に合わせて選択するのが良いと考えます。

このようなpullの処理を完了すると、ようやく正常にpushができます。

強制pushをすることで起きる問題

上記のような、pullの操作が煩雑だと感じる場合、`git push`時に`-f`オプションを付与することで、pull操作をスキップできますが、これは推奨される操作ではありません。

特に複数人で作業をする場合は、避けるべき操作です。ローカルブランチの状態を強制的リモートリポジトリに反映することになるため、リモートリポジトリ上のコミット履歴やデータが消える可能性が大いにあります。

git_push-f

上の図のように`git push -f`を実行した場合、自分の手元に取り込んでいなかったリモートでの変更履歴(コミットC)と該当コミットの変更差分がリモートリポジトリ上から消えます。コミットCをローカルリポジトリに保持している他の開発者がいない場合、コミットCは復旧できなくなります。

エラーの目的

ここまで、Merge時のコンフリクトやpush時の競合について解説しました。

この章では、それらのエラーを発生させる目的について、ブランチの構造とコミットの構造を確認しながら記載します。

ブランチの構造

ブランチを作成し、分岐前と分岐先のブランチそれぞれでコミットを作成すると分岐が発生ます。

ブランチの構造

ブランチを作成するほとんどの目的は、特定の機能の開発やバグ解消のためのコード修正時に、後退処理の余地を残すことであると考えます。つまり、分岐先のブランチでバグ解消などの対応を行い、動作確認や他の開発者によるレビューを済ませた後に、メインのブランチにMergeすることを期待しています。

メインのブランチにMergeする際に、メインブランチに存在する同じファイルを単純に上書きすると、動作確認やレビュー済みのメインブランチのコードが消えてしまいます。

そのような不都合を防ぐために、コンフリクトの発生をエラーとして表示し、Merge処理を完了させない仕組みにしていると考えられます。

ブランチを切らずとも、開発した機能やバグ解消のコードが不適切であると判断された際に、`git reset`や`git revert`を用いて、コードを戻せますが、特に`git revert`を用いると、後退処理に関するコミットが作成されてしまうため、コミット履歴が煩雑になります。

リモートリポジトリでは、`git reset`は実行できないため、複数人で開発を行う場合は、ブランチを切った方が良いことになると言えます。

コミットの構造

コミットは過去の履歴をさかのぼる時に必要な情報です。分岐したコミットを他方の枝にマージしようとするときに、分岐した両方にオリジナルのコミット情報がある場合、両方のコミット状態を残しつつマージしなければ、どちらかのコミット履歴が消えてしまいます。

そうならないために、前述したリベースやマージの処理が必要です。以下はリベースを選択したときの例ですが、このようにもともと存在していた「コミットC」が消え、取り込み元のブランチにのみ存在する「コミットB」をまず取り込んだ後、コミットCと同様の変更差分である「コミットD」が新たに作成されます。

git_pull--rebase

上の図の例の場合、同一のオブジェクト(ファイル)のコンフリクトは発生していません。しかしながら、オプションをつけずに`git pull`をした際に、Gitのデフォルト設定のままでは「適切な対処」の章に掲載したようなエラーが発生します。`git config`にて、`pull.rebase false`または`pull.rebase true`を設定しているとエラーは発生しません。

`git pull`ではなく、`git merge`を実行した場合は、同様のエラーは出ません。`git merge`は、異なるブランチ(例:`test`と`main`)をMergeするときによく利用しますが、`git pull`の場合は、同じブランチ(例:`main`と`origin/main`)をリモートブランチから取り込む目的で使用されます。取り込み前にローカルブランチでオリジナルな変更がされていない前提であるため、pull時に競合が発生することは想定外の事象として扱われるのだと推測します。

`main`と`origin/main`の関係のように、ブランチが同じでも競合が発生するのは、ブランチが実態ではなく、単なるポインタであるためです。ポインタであるがゆえに、ローカルブランチとリモートリポジトリ(リモート追跡ブランチ)の位置がずれている場合、pull時に位置を合わせようとしますが、ローカルブランチの方でリモートリポジトリに存在しないコミットが存在する場合、位置を合わせる前に、分岐した情報の整理が必要です。

もし、ブランチがポインタではなく、独立したコミットであるならば、ローカルブランチとリモート追跡ブランチのそれぞれで同じ内容のコミットが重複して作成され、それらをマージする時に分岐が発生していたら、それは競合ではなく単なるマージ対象として扱われるはずです。

まとめ

ここまでに、Gitで起こるコンフリクトや競合といった問題を解決する方法やそのメカニズムについて記載しました。

問題のスムーズな解消と、本来の目的に集中できるGit知識の習得につながれば幸いです。

エンジニアとしてインフラの運用保守やアプリ開発を行っている。趣味は自転車とサウナ。

最新情報をお届けします!

最新のITトレンドやセキュリティ対策の情報を、メルマガでいち早く受け取りませんか?ぜひご登録ください

メルマガ登録

最新情報をお届けします!

最新のITトレンドやセキュリティ対策の情報を、メルマガでいち早く受け取りませんか?ぜひご登録ください

メルマガ登録