git mergeとは?gitのコミットやブランチの仕組み・マージの発生するpullやpush

git mergeとは?gitのコミットやブランチの仕組み・マージの発生するpullやpush

git mergeとは?


gitではローカルにリポジトリを作り開発の殆どがローカル環境で行われ、ブランチと呼ばれる単位で作業を進めます。

マージとはブランチを統合することで基本的にはそれぞれの編集を足し合わせたものになります。

大まかに新たに作業用のブランチを作って編集し、問題がないことを確認したら本流のブランチにマージさせる流れになります。

またネットワークを利用して複数のローカルリポジトリからそれぞれのブランチをネットワーク上に共用するリモートリポジトリのブランチに足し合わせ開発を進めていくことができます。

リモートリポジトリとローカルリポジトリの間でブランチを更新するgit push、git pullでも内部でマージが行われています。

簡単なgit mergeの使い方

gitを使用しての作業はブランチの中で行われます。ブランチとは開発の本流から分岐して、本流の開発を邪魔することなく作業を続ける機能のことです。

または

などのコマンドで自端末にローカルリポジトリを作成すると最初はmasterブランチのみが存在します。このmasterブランチが開発の本流となるブランチです。

以下リモートリポジトリをリモート、ローカルリポジトリをローカル、masterブランチをマスターと呼び、新しく作成したブランチはブランチ名で呼ぶことにします。

HEADとは

ブランチを確認します。ここでブランチとHEADの意味を確認します。

gitはコミットを差分で管理しません。gitではコミットはツリーオブジェクトと一つ前のコミットへのポインタ(データの位置を示すデータ)などを格納したオブジェクトです。ツリーオブジェクトは変更ごとに圧縮されたファイルの参照とディレクトリの構造などを格納しています。

ブランチとは一連のコミットオブジェクトの先頭を指すポインタです。コミットをするとブランチは自動的に進みます。一番先頭から以前のコミットのポインタをたどり開始点まで戻ることができます。

ブランチが切られると元のブランチの先頭と同一のコミットオブジェクトを指したポインタが作られ、そのブランチでコミットすることで元のブランチとは別に進みます。

git checkout (ブランチ)と入力することで編集するブランチの切り替えができ、編集しているブランチの先頭を指すポインタがHEADです。

新しくブランチを作る(ブランチを切る)

新しくブランチを切って作業を進めます。

このブランチは作業のために短期間使う一時的なブランチでトピックブランチと呼ばれます。ブランチの運用方法は複数あって今回はgit-flowを簡略化したmasterブランチとtopicブランチで構成されるGitHub Flowを採用しています。

移動したブランチadd-modify1内で編集を進めます。

トピックブランチで行われた編集をコミットする

区切りの良いところでtopicブランチで行われた変更をコミットします。gitはコミットする度にディレクトリ下のファイルを圧縮して.gitに格納するので、たとえ一文字編集してコミットしたとしてもコミットの度にデータ量は肥大します。

追跡する必要のないファイルは.gitignoreに記入することをおすすめします。

マスターブランチにマージする

add-modify1からマスターに移動します。

add-modify1をマスターにマージします。

以下同様に編集するためのブランチを切り、マスターにマージするという作業を繰り返します。既にマージされているトピックブランチは削除しても構いません。

このコマンドは既にマージされているブランチにしか適用されません。マージされていないブランチを削除する場合は次のように入力します。

複数のブランチで開発する

同じマスターから複数のトピックブランチを作って異なる機能などを並列に開発することがあります。

それぞれ交互にブランチに入って作業を進めていきます。

先にadd-modify2をマージします。

次にadd-modify3をマージします。

ディレクトリ内のファイルで重複した場所を編集していなければ後て解説するコンフリクトが発生せず無事にマージは成功します。

fast-forwardマージ、non fast-forwardマージとは

add-modify2をマージする時点で、マスターはまだコミットされておらず、コミットの履歴はadd-some2と同じものになります。マスターのHEADはadd-modify2の先頭のコミットオブジェクトに移動するだけで何も作成されません。このようなマージをfast-forwardマージとよびます。

これに対してブランチadd-modify3をマージした時、ブランチadd-modify3が切られた時点からマスターとadd-modify3ブランチのそれぞれの変更の差分を取った新しいファイルが作成されます。

この状態はマスターのポインタを進めるだけでは実現できず、直前のコミットへのポインタを二つ格納した新しいコミットオブジェクトが生成されます。これがnon fast-forwardマージです。

non fast-forwardマージはコミットが発生するのでmerge -m “merge some branch”のようにコミットメッセージを通常のコミットと同じように入力します。

non fast-forwardマージを強制する

ここでadd-modify2をマージするようなケースでも作業上のルールでマスターのマージをコミットとして履歴に残して管理していることがあります。fast-forwardマージが適用されるケースでもnon fast-forwardマージを強制する書き方があります。

このように書くとadd-modify2のマージにもマージコミットとして残ります。

リモートリポジトリからPullする

複数人での開発ではリモートに最新のコミットを登録して、ローカル側はリモートと同期を取る必要があります。ここではリモートのマスターを取得します。マスター以外のブランチを取得することも可能です。

リポートのブランチを取得しローカル側に置きます。

ローカルのブランチにマージします。ここでマージを行うブランチはローカルのマスターです。

git pullとは

以上の操作はこちらのコマンドと等価です。

一回の操作で済むので便利ですが、中止したい場合や元に戻したい場合などの処理は煩雑になります。

ローカルのブランチをリモートにPushする前には必ずリモートのブランチをローカルのブランチにPullして同期させ、その後ローカルのトピックブランチをマージしてリモート側がfast-forwardマージの形になるようにします。

ここでローカルのマスターをリモートのマスターにPushすることを考えます。ローカルのマスターには編集がコミットされたブランチadd-modifyがあります。まずリモートのマスターと同期させます。

次にadd-modifyをマージします。

rebaseとは

先程のgit pullコマンドによって同期させたローカルのマスターにadd-modifyブランチをマージする際、add-modifyのマージコミットが履歴として残ります。

add-modifyブランチのマージコミット履歴はリモートのログに記録すべきローカルで実装したファイルの追加や編集のコミットとは違い開発の流れに関係ない履歴であるとして、リモートにPushする際には取り除くように作業ルールに決められていることもあります。

そこで先程のadd-modifyをマージする代わりにadd-modifyを作り変えます。ローカルのマスターはマージをするだけで自身を編集してコミットしないものとします。

または次のコマンドでここまでのPullからrebaseまでを行えます。

これによってマスターの後にadd-modifyが一連のコミットとなり、先程のマージコミットは生成されません。ただしrebase後のadd-modifyはリベース前とは異なるコミットオブジェクトです。あとはこのブランチをマスターにfast-forwardマージさせPushします。

リモートリポジトリにPushする

複数人でリモートリポジトリを共有して開発を進める際、作業者それぞれがPush(リモートのブランチにローカルのブランチをアップロードしてマージする操作)を繰り返して最終的な成果物になります。

まずローカルのトピックブランチadd-modifyをリモートのマスターでリベースします。

次にローカルのマスターでadd-modifyをマージします。

現在のローカルのマスターはリモートのマスターからadd-modifyのコミットの分だけ進んでいるのでfast-forwardマージの形でリモートリポジトリにPushできます。git pushはfast-forwardマージの形でない場合エラーになり強制的にgit push -fによってPushすることになります。

git mergeを自由自在に使う方法

これまでマージする際にコンフリクトが発生しないものとして説明してきましたが複数人の関わる実際の開発ではコンフリクトは頻繁に発生します。

また間違ったブランチをマージしたり、マージした後でバグが判明するなどマージを取り消す必要が出てくることがあります。

マージをなるべく頻繁に行うことで、コンフリクトの発生やバグなどを早い段階で発見できることになります。

コンフリクトが発生したら

ブランチ内の異なる箇所で変更があった場合にはコンフリクトは発生しません。コンフリクトが発生するのは同一のブランチから切られたブランチで編集された内容が、同一の箇所を編集し内容が異なっている場合です。

また編集だけではなく片方のブランチで編集されたファイルがもう一方では削除されている場合もコンフリクトが発生します。

もちろんもう既にマージされているブランチで行った変更箇所を、新たにブランチを切って編集してもコンフリクトは発生しません。

コンフリクトが発生するのはgit mergeコマンドだけでなくrebaseでも同様に発生しますが対処方法は基本的には同じです。またPushやPullでも発生する可能性があります。

事前に確認する

git diffを使うと重複して編集しているファイルと場所を確認することができます。これからマージするローカルブランチ同士を比較するには

左側のブランチが変更前、右側のブランチが変更後として表示されます。現在いるブランチとリモート(origin)ブランチ(master)と比較するには( origin/master はgit fetchやgut pullで最新の状態に更新されます)

または

もし既存の編集したファイルが片方では存在しない場合や、同一のファイル内の同じ箇所にマイナス記号とプラス記号が隣接して表示されている場合は同一箇所を重複して編集している可能性が高くなるのでコンフリクトの有無を確認してローカルのファイルを編集し直すか、他の作業者のリモートリポジトリの操作ミスを確認したほうがよいでしょう。

状態を確認するコマンド

git diffの表示を見ると注目すべき箇所は—+++ファイル名と書かれている行で2つのファイルの間で異なっている箇所が存在することを示しています。

1番目の引数が変更前で2番目の引数が変更後と見なされます。@@で囲まれた数字は二つのファイルのそれぞれ表示を開始する行と行数を示しています。+が追加された箇所で-が削除された箇所です。

git logはこれまでのリポジトリのコミットの履歴が全て表示され、冒頭の長い英数字はコミットIDと呼ばれリポジトリ内のコミットを一意に識別します。またgit logブランチ名を入力してこのブランチで行われたコミットだけを表示することができます。

先程のgit diffでも二つの時点のコミットIDを指定することによって二つのコミット間の差分を調べることができます(コミットIDは最初の7桁ぐらいで全て入力する必要はありません)。

コミットを引数に取る他のコマンドでもコミットIDでコミット指定することができます。

優先するブランチを指定する

コンフリクトが発生しているファイルでどちらかのブランチを指定することによって、コンフリクトを解決できます。

file1でコンフリクトが発生し現在いるブランチのファイルを取り込む場合は

もう一方のブランチのファイルを取り込む場合は

最後に

 

直接コンフリクトを起こしている箇所を編集する

コンフリクトが発生したらgit statusを入力するとマージに失敗したファイルがUnmerged pathsに表示されます。

直接コンフリクトを起こしているファイルをエディタで開くとマージしようとした重複して編集した箇所が次のように表示されています。

この部分を直接編集してもコンフリクトを解決できます。修正したらコミットします。注意としてこの状態のファイルでも次のコマンドを実行すればコミットは通ってしまいますので必ず編集してコンフリクトを解決してください。

コミットするとマージコミット(non fast-forwardマージ)が生成されます。

git mergetoolを使う

gitではコンフリクトが発生した際の編集ツールとしてマージツールが多数用意されています。編集しながらdiffを確認することができます。

git mergeを取り消す

マージする際またマージした後の何らかの不具合でマージを取り消す方法について紹介します。

マージの段階でコンフリクトが発生し、その時点でファイルの編集せずにマージを中止する場合には次のように入力します。

マージの段階でコンフリクトが発生し、ファイルの編集をしたが結局中止する場合には次のように入力します。

マージを完了した後に取り消す

次にマージを完了しましたが何らかの理由でマージする前の状態に戻すことを考えます。

fast-forwardでマージコミットのないマージをした場合はgit logでマージしたブランチで行われたコミットを全て抽出します。

この方法でマージコミットの取り消しをしてもそれ以降に行われたマージコミットは取り消しされません。またこの方法では取り消したマージコミットとマージコミットを取り消すコミットが二つ履歴に残ります。

この方法だと取り消した同じマージを再び行おうとするとマージができなくなります。一度コミットしてコミットIDを変える必要があります。

次に述べるマージの取り消し方法と比べると手間がかかりますが複数人が使用する共用リポジトリにPushした場合にはこの方法を取るべきです。

完了したマージを履歴ごと無かったことにする

完全にマージとそのコミット履歴を消したい場合に使います。

ORIG_HEADはHEADが移動するコマンドを実行した時、コマンド実行する時点のHEADの位置を記録しています。代わりにログに記載されているコミットのハッシュ値(最初の七桁ぐらい)を引数としてハッシュ値のコミットをした時点に戻ります。また移動させるHeadの位置の指定は^の数でも指定でき、HEAD~数字でも同様の指定が出来ます。例えばHead^^^(Head~3)は3つ前のコミットの直前です。

この方法はローカルリポジトリで行う場合は便利ですが、変更したブランチを他の開発者と共有するリポジトリにgit push -fで強制的にPushすることは避けたほうがいいでしょう。

修正前のブランチをPullしていた他の開発者はローカルに修正前のブランチを取り込んだまま編集を進めてしまい、次のPullで打ち消すことができなくなります。リモートへ取り消しの変更をPushする場合はgit revertを使うことをおすすめします。

まとめ

ここまで読んでいただき誠にありがとうございました。今回はgit mergeを中心にしたgitの操作について解説してきました。

gitはコマンドとオプションが豊富に用意されていますので詳細は

などでコマンドで確認してください。
公式のサイトのドキュメントもお読みください。

インフラエンジニア専門の転職サイト「FEnetインフラ」

FEnetインフラはサービス開始から10年以上『エンジニアの生涯価値の向上』をミッションに掲げ、多くのエンジニアの就業を支援してきました。

転職をお考えの方は気軽にご登録・ご相談ください。