git merge: ブランチの統合とコンフリクトの解決
git merge は、指定したブランチの変更を現在のブランチに取り込みます。現在のブランチが先に進んでいなければ Git はポインタを動かすだけ(fast-forward)、そうでなければ親を2つ持つマージコミットを作ります。目的のブランチに移動してから git merge ブランチ名 を実行します。コンフリクトが起きたら、ファイルを修正して git add と git commit を行うか、git merge --abort ですべて取り消します。
git merge の働き
git merge は、指定したブランチの変更を、現在いるブランチに取り込みます。方向を覚えておきましょう。統合は「自分の中へ」取り込みます。feature/login を main に統合するには、まず main に切り替えてから統合コマンドを実行します。
git switch main
git pull # サーバーから最新の main を取得
git merge feature/login
Gitは2つのブランチの変更を1行ずつ手で書き写すわけではありません。2つのブランチの共通の祖先(merge base)を見つけ、その祖先と両方のバージョンを比較して、変更を組み合わせます。両側が異なる場所を変更した場合は、自動で結果が得られます。同じ場所を変更した場合はコンフリクトが発生し、どちらを採用するかはあなたが決めます。
2つのモード: fast-forward と3方向マージ
fast-forward: マージコミットが不要な場合
たとえば、main から feature を分岐させて3つのコミットを作ったとします。その間、main には誰も何も追加していません。この場合、main の履歴は feature の履歴の始まりにすぎません。Gitは main のポインタを feature の最後のコミットまで進めるだけで、新しいコミットは作りません。
$ git merge feature
Updating 3f2a1c4..9b7e0d2
Fast-forward
src/auth.js | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
履歴は一直線のままですが、「ここでブランチ feature があった」という情報は失われます。それが良いかどうかは場合によります。詳しくは後の --no-ff の説明を参照してください。
3方向マージとマージコミット
両方のブランチが共通の祖先から先に進んでいる場合は、ポインタを単純に動かすことはできません。Gitは3方向マージを行います。共通の祖先(base)、「自分の」側(現在のブランチ)、「相手の」側(統合されるブランチ)を使います。結果は、親を2つ持つ独立したマージコミットとして記録されます。
$ git merge feature
Merge made by the 'ort' strategy.
src/auth.js | 42 ++++++++++++++++++++
src/router.js | 3 +-
2 files changed, 44 insertions(+), 1 deletion(-)
Gitはエディタを開き、Merge branch 'feature' のようなメッセージを表示します。そのまま使っても、統合の理由を追記してもかまいません。分岐と統合の様子は次のコマンドで確認できます。
git log --oneline --graph --all
Git 2.34以降、既定のマージ戦略は ort です。以前は recursive でした。日常の作業で戦略を変える必要はありません。
git merge の便利なオプション
| オプション | 動作 | 使いどころ |
|---|---|---|
--no-ff | fast-forward が可能でも、必ずマージコミットを作る | 1つの機能に属するコミットのまとまりを履歴で見たいとき |
--ff-only | ポインタを進めるだけで統合し、それができなければエラーで止まる | 不要なマージコミットを作らずにローカルブランチを更新するとき |
--squash | ブランチのすべての変更をインデックスに集めるが、コミットはせず、ブランチとの関係も記録しない | 機能を1つのきれいなコミットにまとめたいとき |
--no-commit | 統合は行うが、コミットの直前で止まる | コミットする前に結果を確認したり、テストを実行したりしたいとき |
-m "テキスト" | マージコミットのメッセージを指定する | エディタを使わないスクリプトなど |
--abort | 進行中のマージを中止し、マージ前の状態に戻す | コンフリクトが多すぎて、やり直したいとき |
--continue | コンフリクトを解決した後でマージを完了する | 最後の手順で git commit の代わりに使う |
-X ours / -X theirs | コンフリクトした部分を、自分側または相手側の版で自動的に解決する | どちらのバージョンが正しいか確実な場合のみ(注意して使う) |
--no-ff: 機能の境界を残す
多くのチームは、機能のブランチを --no-ff でのみ統合します。こうすると、グラフ上で各タスクが1つの「まとまり」として見え、git revert -m 1 <マージコミット> だけで丸ごと取り消せます。fast-forward の場合はそのような境界がないので、コミットを1つずつ取り消す必要があります。
--squash: 10個のコミットを1つにまとめる
git switch main
git merge --squash feature/login
git commit -m "メールアドレスとパスワードによるログインを追加"
--squash の後、Gitはブランチを統合済みとは見なしません。そのため git branch -d feature/login は「not fully merged」という警告とともに削除を拒否します。内容を確認したうえで git branch -D を使って削除してください。また、squash の後に同じブランチで作業を続けないでください。次の統合で既に取り込んだ変更が再び持ち込まれ、ほぼ確実にコンフリクトが起きます。
マージのコンフリクトを解決する
コンフリクトは事故ではなく、日常の一部です。2つのブランチが同じ行を変更したとき(または、一方が別の側で編集されたファイルを削除したとき)に発生します。どちらのバージョンが正しいかをGitは推測しないため、判断は人が行います。
$ git merge feature/login
Auto-merging src/config.js
CONFLICT (content): Merge conflict in src/config.js
Automatic merge failed; fix conflicts and then commit the result.
手順1: 何が競合しているかを確認する
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/config.js
手順2: ファイルを編集する
コンフリクトしたファイルには、Gitがマーカーを挿入します。
<<<<<<< HEAD
const TIMEOUT = 5000;
=======
const TIMEOUT = 10000;
>>>>>>> feature/login
上の部分が現在のブランチ(HEAD)、下の部分が統合されるブランチです。正しい方を残すか(または両方を組み合わせて)、3行のマーカーをすべて削除して、ファイルを保存します。よくあるミスは、ファイルのどこかにマーカーを残したままにしてしまうことです。コミットする前に git diff --check で残っているマーカーを検出できます。
手順3: 解決を記録してマージを完了する
git add src/config.js
git commit # または git merge --continue
コンフリクト全体を片方の側の版で解決するなら、ファイルを手で編集する必要はありません。
git checkout --ours src/config.js # 現在のブランチの版を残す
git checkout --theirs src/config.js # 統合されるブランチの版を採用する
git add src/config.js
エディタを使うと楽
VS Code、JetBrains のIDEなどは、コンフリクトを3つの列で表示し、「現在の変更を採用」「受信した変更を採用」「両方採用」のボタンを提供します。外部ツールを git mergetool で接続することもできます。役立つ設定は git config --global merge.conflictStyle zdiff3 です。マーカーに共通の祖先の版も表示されるため、どちらが何を変えたのかが分かりやすくなります。
やめたくなったら: マージを取り消す
git merge --abort
このコマンドで、作業ディレクトリとインデックスは git merge の前の状態に戻ります。そのため、統合の前には作業ディレクトリをきれいにしておくとよいでしょう。未完成の変更はコミットするか、git stash で退避しておきます。そうしないと、その変更と統合の結果を区別しにくくなります。
完了したマージを取り消す
マージコミットがまだサーバーに送られていなければ、ブランチを戻すのが最も簡単です。
git reset --hard ORIG_HEAD
Gitは、マージなどの危険な操作の前に ORIG_HEAD を記録します。これはマージ前のブランチの位置を指します。--hard は未コミットの変更を破棄するので、先に git status で確認してください。
マージがすでに共有リポジトリにある場合は、履歴を書き換えず、逆向きのコミットを作ります。
git revert -m 1 a1b2c3d
-m 1 は、「本流」として最初の親(統合先のブランチ)を指定します。注意点があります。このように revert した後、Gitは機能のコミットがすでに統合済みだと見なします。修正した機能を後で統合し直す場合は、まず revert のコミットを revert する必要があります。
チームでのマージ
git pull は fetch と merge の組み合わせ
通常の git pull は変更を取得し、リモートのブランチをローカルに統合します。あなたと同僚が同時にコミットすると、「Merge branch 'main' of ...」というコミットが生まれます。こうした技術的なマージを避けるには、git config --global pull.ff only を設定して、fast-forward できない場合は pull を失敗させ、どうするかを自分で決めるようにするか、git pull --rebase を使います。
pull request と Merge ボタン
GitHub や GitLab では、統合は通常 pull/merge request を通して行われます。「Create a merge commit」は git merge --no-ff に、「Squash and merge」は squash 統合に、「Rebase and merge」は rebase に相当します。どの方式を選ぶかはチームの取り決めです。仕組み自体はコマンドラインと同じです。
自分のブランチに main を取り込む
長く続くブランチは main から離れていき、コンフリクトが蓄積します。定期的に主要ブランチを自分のブランチに取り込みましょう。
git switch feature/login
git fetch origin
git merge origin/main
こうすると、コンフリクトは発生したときに小さく解決され、最後に main へ統合するときもスムーズに進みます。
よくあるミス
- 逆向きに統合してしまう。
mainにいる状態でgit merge mainを実行したり、機能ブランチを別の機能ブランチに統合したりする。統合の前にgit branch --show-currentで現在のブランチを確認しましょう。 - 古いブランチに統合する。 1週間更新していないローカルの
mainを統合してしまう。先にgit pull、その後に merge。 - 「Your local changes would be overwritten by merge」。 同じファイルに未コミットの変更がある。コミットするか、stash に退避する。
- 「refusing to merge unrelated histories」。 ブランチに共通の祖先がない。たとえば、ローカルのリポジトリを
git initで作り、リモートにはすでに README がある場合。統合が本当に必要なら--allow-unrelated-historiesを追加します。 - コンフリクトマーカーをコミットしてしまう。 ビルドが
<<<<<<<で失敗する。コミットの前にgit diff --checkで確認し、テストを実行してください。
まとめ
git merge は安全にブランチを統合する方法です。既存のコミットを書き換えず、新しいコミットを追加するか(またはポインタを動かすだけ)です。現在のブランチで統合し、作業ディレクトリをきれいに保ち、機能の境界を残したいときは --no-ff、1つのコミットにまとめたいときは --squash を使いましょう。コンフリクトは3つのステップで解決します。ファイルを修正し、git add し、git commit します。git merge --abort を使えば、いつでも出発点に戻れます。ブランチの統合とコンフリクト解決は、Gitコースで実際に練習できます。
よくある質問
git merge と git pull の違いは何ですか?
git pull はまずサーバーから変更を取得し(git fetch)、その後リモートのブランチを現在のブランチに git merge、または設定により rebase で統合します。git merge は、ローカルにすでにあるものだけを扱います。
複数のブランチを一度に統合できますか?
はい。git merge feature-a feature-b は、親を複数持つ octopus マージを行いますが、コンフリクトがない場合に限られます。実際には、ブランチは通常1つずつ統合します。
どのブランチがすでに現在のブランチに統合されているか、どう確認しますか?
git branch --merged は現在のブランチに完全に取り込まれたブランチを表示し、git branch --no-merged はまだ統合されていないブランチを表示します。統合済みのブランチは git branch -d で安全に削除できます。
統合の前に、何が取り込まれるかを確認するには?
git log main..feature で、main にはないコミットを確認できます。git diff main...feature で、共通の祖先から見たブランチの変更を確認できます。
merge の後の ORIG_HEAD とは何ですか?
マージの前にGitが保存する参照で、マージ前のブランチの位置を指します。サーバーに送っていないローカルのマージは、git reset --hard ORIG_HEAD で取り消せます。