メインコンテンツまでスキップ

GitとGitHubを用いたワークフロー

現代のチーム開発では、通常Gitが用いられます。Gitの概念は複雑ですが、チーム開発で起こる様々な状況に適切に対処するためには、ある程度の理解が必要となります。この節では、Gitの思想を解説したうえで、GitHubを用いてチーム開発を行う手法を示します。

コミットが記録される仕組み

Gitの節では、Gitのコミットに一意のIDが割り当てられることを説明しました。実は、コミットIDは、次の情報から計算可能です。つまり、次の情報が完全に一致しているのであれば、どのような環境でコミットを行なっても同じコミットIDが割り当てられます。逆に、次の情報のうち一つでも異なるものがあれば、全く違うコミットIDが割り当てられます。

  • すべてのファイルやディレクトリの名前
  • コミットの作成者の名前やメールアドレス
  • コミットが作成された日時
  • コミットメッセージ
  • 親コミット (ひとつ前のコミット) のID

これらの情報の中に、リポジトリは含まれていません。コミットは、リポジトリとは独立して存在するものなのです。

また、注目したいのは、コミットの情報の中に親コミットのIDが含まれているところです。つまり、歴史の流れの方向と、コミットグラフの参照の方向は逆向きになります。この性質により、一度作成されたコミットはその後の変更に影響を受けません。

歴史の流れと参照の方向

ブランチとHEAD

ブランチは、ソースコードへの変更の枝分かれを扱うための仕組みです。ブランチはリポジトリの中に存在し、コミットを指し示します

各リポジトリには、HEADと呼ばれる、現在実際にディレクトリに表れている状態を表す特殊なポインタがあります。作業中のブランチがある場合、HEADはそのブランチを指し示します。git init コマンドによりリポジトリを新しく作成した場合、HEADは自動的に master ブランチを指すように設定されます。

HEADが master ブランチを指している状態で、コミットを行った際に起こる変化を表したのが次の図です。直前まで master ブランチが指していたコミット 2ce3d099 を親とする新しいコミット cee8a14f が作成され、ブランチ master が指し示す先は新しく作成されたコミットに変更されます。

コミット

別のブランチで作業する

新しいブランチを作成する場合には、git checkout -b コマンドを実行します。次の図は、HEADがコミット 2ce3d099 を指している状態で、git checkout -b feature を実行した例です。直前までHEADが指していたコミットを指し示すブランチ feature が作成され、HEADが指し示す先も新しいブランチに変更されます。

チェックアウト

この状態で新しいコミット cee8a14f を作成すると、feature ブランチが指し示す先のみが新しいコミットに変更されます。

作成したブランチでコミット

ここで git checkout master を実行すると、HEADの指し示す先のみが master ブランチに変更され、ディレクトリ内のファイルはコミット 2ce3d099 のものに戻ります。

masterブランチに戻る

このまま、さらにコミット bfaaf878 を追加します。これにより、コミットグラフの枝分かれが生じます。これが、複数人で同時に開発が行われている状態です。

枝分かれしたコミットグラフ

ここまでの操作を実際に行った様子が次の動画で確認できます。

枝分かれしたブランチをマージする

git merge コマンドを用いると、現在のブランチに他のブランチの変更を取り込むことができます。次の例では、HEADが master ブランチにある状態で、git merge feature を実行することで feature ブランチを master ブランチにマージしています。

マージ

このマージを実行すると、bfaaf878cee8a14f の2つの親を持つマージコミット d021150b が生成され、2つのブランチ両方で行われた変更を含むコミットとなります。マージコミットのコミットメッセージは自分で指定することもできますが、Git側で用意してくれる標準のメッセージ (この例では Merge branch 'feature') をそのまま用いても良いでしょう。

Git標準のエディタ

コマンドラインからコミットを作成する際、-m オプションを指定しなかった場合、コミットメッセージを編集するためのエディタが起動します。このエディタは自分で設定することができますが、上の例ではnanoが起動しており、この場合はcontrol + X (macOS) / Ctrl + X (Windows) で終了します。

環境によってはVimが起動する場合があります。この場合は、:q を入力して Enter を押下することにより終了できます。

コンフリクト

git merge コマンドが実行されると、Gitはまずコミットグラフ上の共通の祖先を探します。例えば、コミットグラフが次のような状態であるとき、Gitは master ブランチと feature ブランチの共通の祖先であるコミット 2ce3d099 を起点とした変更を取得します。

共通の祖先 (2ce3d099)
<li>吾輩は猫である</li>
<li>坊っちゃん</li>
master (0d4cba5c)
<li>吾輩は猫である</li>
<li>坊っちゃん</li>
<li>三四郎</li>
feature (f08f242a)
<li>吾輩は猫である</li>
<li>坊っちゃん</li>
<li>こころ</li>

コンフリクト

この例の場合、共通の祖先に対して master<li>三四郎</li> が、feature<li>こころ</li>同じ場所に追加されています。この状態で git merge feature を実行すると、Gitはコンフリクトを報告し、マージを中断します。コンフリクトが発生したファイルには、Gitにより自動的に <<<<<<<=======>>>>>>> といったコンフリクトマーカーが挿入されます。

<li>吾輩は猫である</li>
<li>坊っちゃん</li>
<<<<<<< HEAD
<li>三四郎</li>
=======
<li>こころ</li>
>>>>>>> feature

コンフリクトを解決するには、ファイルを編集してコンフリクトマーカーを削除する必要があります。全てのコンフリクトに対応できたら、コンフリクトしたファイルをステージし、git merge --continue コマンドを実行してマージを続行しましょう。

ここまでの操作を実際に行うと、次の動画のようになります。

リモートブランチ

GitとGitHubの節では、自分のPCに置かれたリポジトリ (ローカルリポジトリ) とGitHub上のリポジトリ (リモートリポジトリ) を接続しました。git push origin master コマンドを行ったときのGitの動作を確認しておきましょう。

git push origin master コマンドは、ローカルリポジトリの master ブランチが指し示すコミットを、リモートリポジトリの master ブランチが指し示すコミットとして設定するためのコマンドです。次の図は、ローカルリポジトリの master ブランチがコミット 2ce3d099 を指している状態で、空のリモートリポジトリ origin に対して git push origin master を実行した際の様子を表しています。

リモートブランチ

この状態でコミットを行うと、ローカルリポジトリの master ブランチが、リモートリポジトリの master ブランチより1コミット分進んでいる状態になります。

1コミット進んだ状態

再び git push origin master を実行 (最初のpush時に -u オプションを指定した場合は git push) することで、作成したコミットをリモートリポジトリに反映させられます。

再びプッシュする

ここまでの操作を実際に行うと、次のようになります。

他の人が行った変更を取得する

自分以外がリモートリポジトリに対して変更を加えた場合、リモートリポジトリのブランチがローカルリポジトリのブランチより先のコミットを指している状態になります。git pull コマンドにより、ローカルリポジトリのブランチが指し示す先を、リモートリポジトリのブランチが指すコミットと一致させることができます。次の例では、git pull origin master により、ローカルリポジトリの master をリモートブランチ master の最新のコミットと一致させています。

プル

プルの際にマージが必要な場合

自分が最後に git pull をした後に他の人がリモートリポジトリにプッシュした状態で、自分が新しいコミットを作成すると、次の図のような状態になります。

マージが必要

この状態で git pull を行うと、自動的にマージコミットが作成されます。

自動的に作成されるマージコミット

再び git push origin master を実行することにより、変更を正しくリモートブランチに反映できます。

自動的に作成されるマージコミット

ここまでの操作を実際に行うと、次の動画のようになります。

プルリクエスト

GitHubなどのサービスを用いて共同開発を行う場合、通常は master ブランチへのマージをWeb画面上で行い、Gitのコマンドで master ブランチを操作することはありません。これにより、プログラムの変更が無秩序に行われることを防ぐことができます。GitHubでは、プルリクエストと呼ばれる機能により実現できます。

次の図のようなコミットグラフがある状態を考えてみましょう。master ブランチから feature ブランチを切り出し、作成したコミットをリモートリポジトリにプッシュした状態です。feature ブランチから master ブランチに対してプルリクエストを作成することで、feature ブランチで行った変更を他のユーザーに確認してもらうことができます。

プルリクエスト

プルリクエストをマージすると、ローカルリポジトリで git merge コマンドを実行した場合と同様にマージコミットが作成されます。

プルリクエストのマージ

ローカルリポジトリで再び master ブランチをチェックアウトし、git pull origin mastermaster ブランチをGitHub上の最新のコミットに合わせれば、開発を再開できます。

マージされたプルリクエストをプルする

ここまでの操作を実際に行うと、次の通りになります。

プルリクエストでコンフリクトが発生した場合

プルリクエストでコンフリクトが発生した場合、ローカルではマージコミット作成前に修正をしていましたが、プルリクエストを用いた開発においては、master ブランチは直接操作できないため、代わりにプルリクエストを出した側のブランチを操作して master ブランチにマージ可能になるよう修正します。

次のコミットグラフを考えてみましょう。master ブランチが 2ce3d099 だった際に feature ブランチを切り出し、コミット f08f242a を作成しましたが、他のチームメンバーの開発の結果GitHub上の master ブランチが 0d4cba5c に進み、feature ブランチから master ブランチへのプルリクエストがコンフリクトしている状態です。

コンフリクトの発生したプルリクエスト

この状態を解消するために、ローカルで最新の master ブランチを feature ブランチにマージします。まずは master ブランチをチェックアウトし、最新の master への変更を git pull origin master によりローカルリポジトリに取り込んだうえで、再び feature ブランチに戻ります。

masterブランチを最新にする

ここで git merge master を実行し、コンフリクトを解決して master ブランチとのマージコミット d6d38e90 を作成してプッシュします。これにより、feature ブランチは master ブランチにマージ可能となり、コンフリクトが解消されます。

masterブランチをfeatureブランチにマージ

通常通りプルリクエストをマージすれば完了です。同じように作成されたマージコミットをローカルに取り込むことができます。

コンフリクトが解消されたプルリクエストをマージ

ここまでの操作を実際に行った様子が次の動画になります。

課題

  1. 同一のGitHubリポジトリに対し、同じ行を変更するプルリクエストを2人で作成しましょう。
  2. 片方をマージすると、もう片方のプルリクエストがコンフリクト状態になることを確認しましょう。
  3. コンフリクトを解決しましょう。
  4. マージされたプルリクエストで行われた変更をプルしましょう。