clock-up-blog

go-mi-tech

Gitのマージ概要および、共通の分岐元を持たないブランチ同士のマージ(割としょうもない話)

Git Advent Calendar 2018 - Qiita 1日目の記事になります。ぜんぜん登録ないので助けてほしい……。

それはそれとして今回のお話。まず使うことは無いだろうけど、知っておくといつか役に立つかもしれない(?)トリッキーなマージを紹介します。

そもそも Git のマージとは

Git におけるコミットリストは一般的に様々な理由で分岐します。

分岐した、いわゆるブランチは分岐元にそのブランチ側に積まれたコミット群を取り込む目的で「マージ」という統合操作が行われのが一般的な運用でしょう。

f:id:kobake:20181201190923p:plain

ここに出した例は、いわゆる機能ブランチの例であり、その機能が完成したことをもってして元のブランチにマージしています。ブランチの利用のされ方は様々で、それが例えばリリースブランチであれば分岐しっぱなしで元の派生とは延々絡まないケースもあるにはあります。が、それはまた別の話。今回はマージの話。

よくあるマージの誤解イメージ

「マージ」が示す「統合する、溶け合わせる」的な言葉のニュアンスに引きずられて、マージの挙動を誤ったイメージで理解している方をときおり見かけます。ここは先を打ってマージの正しいイメージを共有しておきます。

「マージ」についてのよくある誤解例

「マージ」をこういうイメージで捉えているとしたら、それは間違いです。
f:id:kobake:20181201190453p:plain
(特殊なオプションを指定しない限り、)「マージ」はこのような破壊的な既存コミットの変更をしません。(そもそもコミットオブジェクトは不動であり絶対に変更できない仕組みになっているのだが。そこの詳しい話は別の機会にでも。)

正しい「マージ」のイメージ

「マージ」が行うのは実際にはこれだけです。
f:id:kobake:20181201190510p:plain
既存コミットには手を付けずに、単に2つの既存コミットへの参照を持つ「マージコミット」を作っているだけ。このマージコミットの存在により一度分岐した歴史が再度統合されたとみなされる、これが「マージ」です。

マージコミット

イメージで示すと、「マージコミット」は本当にただただ既存コミット2つを親コミットとして参照しているだけ。とっても謙虚。
f:id:kobake:20181201192728p:plain
歴史の統合によりしばしば「コンフリクト」が発生し、それを手動で修正してあげる必要があることもありますが、そういった手動修正内容も「マージコミット」に含まれることになります。そこの詳細も書き始めると長くなるので略。

マージの(一般的な)制約

「マージ」をするためには「マージ元」「マージ先」の2つのコミットが指定されます(一般的にはコミットIDではなくブランチ名で指定するのだが、実はこれは同義)。

普通の Git 運用をしていれば、どのコミットであっても過去を辿っていけば確実に同じ祖先コミットを見つけることができるはずで、これがいわゆる「分岐点」として把握され、マージのロジックはその情報を頼りによしなに統合を行ってくれます(実際にはコンフリクトの検出を行ってくれます)。

逆にいうと、共通の祖先コミットが無いコミット同士ではマージをすることはできません(というかそんなケースは普通に考えてまず発生しないので、マージできる必要性がそもそもない)。
f:id:kobake:20181201205906p:plain

本題:共通の分岐元を持たないブランチ同士のマージ

独立ブランチの作成

と、ここまで正論を述べておきながら、上述の「普通に考えてまず発生しない、共通の祖先コミットが存在しないケース」は意図的に作り出すことができます。実はブランチ作成時に「--orphan」オプションを付けると、完全に空っぽのブランチを作れます。つまりこれは既存ブランチとは完全に独立した(同じコミットをひとつたりとも共有しない)ブランチになります。

$ git checkout --orphan BranchX

共通の祖先コミットが無いブランチ同士のマージ

共通祖先の無いブランチ同士を普通にマージをしようとすると、「関連の無いもの同士をマージすることはできない」という意味合いのエラーが発生しますが、

$ git merge BranchX
fatal: refusing to merge unrelated histories

実は git merge コマンドに「--allow-unrelated-histories」オプションを付けると、関連の有無に関わらず2つのコミットを繋げるマージコミットが生成されます。つまりはこういったケースでのマージは普通に実行すると失敗しますがオプション付ければ成功させることができてしまいます。

サンプル

cd
rm -rf strange-merge
mkdir strange-merge
cd strange-merge
git init
echo master1 >> master1.txt && git add . && git commit -m "master1"
sleep 1
echo master2 >> master2.txt && git add . && git commit -m "master2"
sleep 1

git checkout --orphan BranchX
echo x1 >> x1.txt && git add . && git commit -m "x1"
sleep 1
echo x2 >> x2.txt && git add . && git commit -m "x2"
sleep 1

git checkout master
git merge --allow-unrelated-histories --no-edit BranchX
sleep 1

はい完成。上のコマンドをそのままコピペで実行すると、通常はやらないようなトリッキーなマージが行われた状態のリポジトリが手元にできます。SourceTree 等で分岐を見てみると、まさに想定通りの統合が発生していることが見てとれます。
f:id:kobake:20181201201215p:plain

ここにも同じものを push してあります。(ちょっと後から README とか加えてますが)
github.com

このしょうもないマージをいつ使うのか

実際のところ、こんなトリッキーなオプションは一生使わないで済む人のほうが多いでしょう。ネタとして覚えておくくらいで良し。

とにかく Git は構造はシンプルであるにも関わらず、操作コマンドとそのオプションが膨大にあり、全てを把握している人はものすごく少ないでしょう。とはいえ知識欲がある限りはしょうもないコマンドもちょっと触っておくと、その引き出しの数がいつか自分か誰かを助けることになります。たぶん。たぶん。

実際のところ、自分は記憶が曖昧ですが必要に駆られて昔このオプションを活用した記憶がおぼろげにあります。パっと思いつく適用例としてはファイル構造の被らない複数リポジトリをコミットログを維持したまま統合する用途として使うこと等はできそうです。

この記事を読んでしまった奇特な皆様方も、しかるべきときに今回の内容を思いだしてあわよくば現実的な活用をすることもできたら良いですね。