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
のものに戻ります。
このまま、さらにコミット bfaaf878
を追加します。これにより、コミットグラフの枝分かれが生じます。これが、複数人で同時に開発が行われている状態です。
ここまでの操作を実際に行った様子が次の動画で確認できます。
枝分かれし たブランチをマージする
git merge
コマンドを用いると、現在のブランチに他のブランチの変更を取り込むことができます。次の例では、HEADが master
ブランチにある状態で、git merge feature
を実行することで feature
ブランチを master
ブランチにマージしています。
このマージを実行すると、bfaaf878
と cee8a14f
の2つの親を持つマージコミット d021150b
が生成され、2つのブランチ両方で行われた変更を含むコミットとなります。マージコミットのコミットメッセージは自分で指定することもできますが、Git側で用意してくれる標準のメッセージ (この例では Merge branch 'feature'
) をそのまま用いても良いでしょう。
コンフリクト
git merge
コマンドが実行されると、Gitはまずコミットグラフ上の共通の祖先を探します。例えば、コミットグラフが次のような状態であるとき、Gitは master
ブランチと feature
ブランチの共通の祖先であるコミット 2ce3d099
を起点とした変更を取得します。
<li>吾輩は猫である</li>
<li>坊っちゃん</li>
<li>吾輩は猫である</li>
<li>坊っちゃん</li>
<li>三四郎</li>
<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コミット分進んでいる状態になります。
再び 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 master
で master
ブランチをGitHub上の最新のコミットに合わせれば、開発を再開できます。
ここまでの操作を実際に行うと、次の通りになります。
プルリクエストでコンフリクトが発生した場合
プルリクエストでコンフリクトが発生した場合、ローカルではマー ジコミット作成前に修正をしていましたが、プルリクエストを用いた開発においては、master
ブランチは直接操作できないため、代わりにプルリクエストを出した側のブランチを操作して master
ブランチにマージ可能になるよう修正します。
次のコミットグラフを考えてみましょう。master
ブランチが 2ce3d099
だった際に feature
ブランチを切り出し、コミット f08f242a
を作成しましたが、他のチームメンバーの開発の結果GitHub上の master
ブランチが 0d4cba5c
に進み、feature
ブランチから master
ブランチへのプルリクエストがコンフリクトしている状態です。
この状態を解消するために、ローカルで最新の master
ブランチを feature
ブランチにマージします。まずは master
ブランチをチェックアウトし、最新の master
への変更を git pull origin master
によりローカルリポジトリに取り込んだうえで、再び feature
ブランチに戻ります。
ここで git merge master
を実行し、コンフリクトを解決して master
ブランチとのマージコミット d6d38e90
を作成してプッシュします。これにより、feature
ブランチは master
ブランチにマージ可能となり、コンフリクトが解消されます。
通常通りプルリクエストをマージすれば完了です。同じように作成されたマージコミットをローカルに取り込むことができます。
ここまでの操作を実際に行った様子が次の動画になります。
課題
- 同一のGitHubリポジトリに対し、同じ行を変更するプルリクエストを2人で作成しましょう。
- 片方をマージすると、もう片方のプルリクエストがコンフリクト状態になることを確認しましょう。
- コンフリクトを解決しましょう。
- マージされたプルリクエストで行われた変更をプルしましょう。