前回の課題の解説とポイント
シナリオ1 : 変更点を確認しながら、一部だけコミットする
$git init
$echo fileA > A.txt
$echo fileB > B.txt
$git add A.txt B.txt
$git commit -m "first commit"
$echo fileA edit > A.txt
$echo fileB edit > B.txt
$git diff //修正点確認
$git add A.txt B.txt
$git diff //ステージングエリアとワーキングツリーのdiffは何も表示されない
$git diff --cached //ステージングエリアとHEADのdiffを確認
$git reset HEAD B.txt //B.txtのステージングエリアに入れた修正をもとに戻す
$git commit -m "Commit only A's edit"
$git checkout -- B.txt //ファイルをステージングエリアから戻す
$git status //B.txtがもとに戻っていることを確認
シナリオ 2 : ブランチを利用した開発
$git init
$echo fileA > A.txt
$echo fileB > B.txt
$git add A.txt B.txt
$git commit -m "first commit"
$git branch branch1 //とりあえず今の位置にブランチ作成
$echo 1st edit on A > A.txt //masterに2回コミットを作成
$git commit -a -m "first edit on master"
$echo 2nd edit on A > A.txt
$git commit -a -m "second edit on master"
$git checkout branch1 //先程作ったbranch1の位置に移動
$echo 1st edit on B > B.txt //branch1に2回コミットを作成
$git commit -a -m "first edit on branch1"
$echo 2nd edit on B > B.txt
$git commit -a -m "second edit on branch1"
$git checkout master //masterに戻って
$git merge branch1 //branch1をマージ
$git log --graph //コミットグラフを確認
$git checkout -b branch2 [commit hash] // 上のgit logでコミットハッシュを確認し
// ブランチを作成しながらcheckout
$echo fileC > C.txt // ファイルCを作成
$git add C.txt
$git commit -m "Add C.txt" // コミットを作成
$git checkout master // masterに移動して
$git merge branch2 // branch2をマージ
$git branch -d branch2 // branch2を削除
$git log --graph // 期待通りになっているか確認
シナリオ3 : 間違ったブランチに入れたコミットを修正
$git init
$echo fileA > A.txt
$echo fileB > B.txt
$git add A.txt B.txt
$git commit -m "first commit"
$echo editA > A.txt
$git commit -a -m "edit A" //master上に2つ目コミット作成
$git branch my_branch //現在のHEADにmy_branch作成
$git reset --hard HEAD~ //masterをHEADのParentの位置に移動
$git checkout my_branch //my_branchに移動して
$git log //期待通りのコミットグラフになったか確認
gitでチーム開発
今回はチームでの開発についてgitがどのように利用されるのか理解していきたいと思います。
すべてを一回で説明するのは大変なので基本的な原理を今回理解して、複雑なシナリオなどは次回に回そうと思います。
gitを使ってチーム開発をする場合GitHubを用いている人たちがほとんどなのではないかと思いますが、ここではGitHubは用いません。GitHubを用いると本質が見えづらくなるためです。GitHubなしでも十分にチーム開発が行なえます。GitHubが停止したら開発が進まなくなるなんてことはgitの場合ありません。
考え方
これまでは、一人で一つのリポジトリ内で操作が完結していました。チーム開発では同じことを複数のリポジトリを使いながら行い管理を進めます。
第一回でも紹介しましたが、gitでのチーム開発はSubversionなどのそれとイメージが違います。図で表すと、Subversionは中央に共有リポジトリを持っていたのに対し、gitはそれがありません。変わりにそれぞれがリポジトリを持ち管理するという方法を取ります。
上の図のようにgitでは複数のリポジトリがお互いにコミュニケーションをとりながら開発を進めることになりますが、まずは、最小単位である二人でのチーム開発がどういう原理で行われるのか見ていきます。
まず、再度一つリポジトリを作成しコミットを一つ作ります。
$ mkdir MyRepo // リポジトリ用の新たなディレクトリ
$ cd MyRepo
$ git init // リポジトリ初期化
$ echo 1st file > text1.txt // ファイルを作成して
$ git add text1.txt
$ git commit // コミット
これまで説明したとおり、gitにおいてはすべてはコミットで管理されます。コミットさえあれば、その状態を復元できます。コミットに関する情報はすべて.git内に含まれているので基本的にそれを別ディレクトリにコピーすることで、2箇所で同じ状態から開発を進めるようにすることができます。Linuxのcp
コマンドでも良いのですが、一部コピーしないほうがいいものや、別の設定にしておいた方が良いファイルがあるので、gitにはそれ用のコマンドgit clone
が用意されています。(原理的には.gitをただコピーしているだけだと思って問題ないです。実際に.gitを手動でコピーしても設定などをきちんとすればcloneと同じことができます。)
$ cd .. // 一度MyRepoの外に出て
$ git clone MyRepo MyRepo_Clone // MyRepoをcloneしてMyRepo_Cloneを作成
これで、同じコミットを持つ2つのリポジトリが出来上がりました。
今回はワーキングツリーやステージングエリアは重要ではないのでコミットグラフのみ示していきます。C1
,C2
などはコミットハッシュを表します。
普段git cloneはURLを指定することが多いと思いますが、このようにgitリポジトリのディレクトリを指定することもできます。要は.gitさえコピーできればよいのです。HTTPSやSSHなどgitがサポートするプロトコルで.gitの内容(=ファイル)をコピーできる条件さえ整えばcloneやこの後行うリポジトリ間でのコミットのやり取りは可能です。
これら2つのリポジトリはこの時点で基本的に独立したものになります。今回は動きを見るために同じPC、さらには同じディレクトリ内にリポジトリをcloneしましたが、通常は全く別々のPC内に存在し、別々の人がこのリポジトリ上で作業していると考えてください。
ここで、MyRepo_Cloneのリポジトリでコミットを作成します。
$ cd MyRepo_Clone
$ echo work in clone > text1.txt // 適当に編集して
$ git commit -a // ワーキングツリーから直接コミット
この新しいコミットはMyRepoリポジトリにはありませんが、別リポジトリからコミットをコピーするコマンドgit fetch
を利用して取得することができます。
$ cd ../MyRepo // MyRepoに戻り
$ git fetch ../MyRepo_Clone [C2] // コミットを取得
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ../MyRepo_Clone
* branch [C2] -> FETCH_HEAD
(C2
の部分には実際のコミットハッシュが入ります。MyRepo_Clone側でコミットハッシュを確認して入力してください)
これにより、MyRepo側にも同じコミットグラフができました。
このリポジトリ間でのコミットのコピーがチーム開発の基本的な動きになります。これさえできれば、自分の作成したコミットだけでなく、人の作ったコミットも自分のリポジトリに取り込むことができます。
コミットグラフは同じですが、それぞれのmasterの位置、HEADの位置が異なることに注意してください。MyRepoから見ればMyRepo_Cloneの新たなコミットは自分のmasterとは基本的に関係がありません。たまたまもう一つの方のリポジトリでそこにmasterというブランチがあるというだけです。gitはこれらの名前が一致しているから同じものであるというような判定はしません。ブランチがどこでも指せるポインタであることを考えれば自然なことかと思います。
またC2
のコミットは必ずC1
の子供になります。コミットにはParentが記録されているのでC2
を取得しようとすればC1
もまた必要になります。今回はMyRepo側にすでにC1
があるのでコピーはされませんが、存在しない場合にはC1
もコピーされます。
言い方を変えると、あるコミットを取得したとき、そこから辿れるすべての先祖のコミットは両リポジトリ間すべて一致しています。
この辺の動きは次のシナリオでも見ていきます。
ここから先、MyRepo側でどうするかはMyRepo側で決めることになります。一般的な手順としては、まず取ってきたコミットにブランチを作成し、checkout、内容を確認して、masterにマージする必要があるならマージする、という感じになります。
$ git checkout -b new_branch [C2] // ブランチを作成してそれをcheckout
// ここで中身を確認
$ git log // 自分の持っている他のコミットとの関係性を確認
// 特に問題なければマージ
$ git checkout master
$ git merge new_branch // このマージは新たなコミットを作る必要がないので、ブランチが移動するだけ
$ git branch -d new_branch // 今後必要なければnew_branchは削除
上のgit merge new_branch
の部分はまだ説明していない動きですが、ひとまず今はこんな風にリポジトリ間でコミットをやり取りして、ブランチを移動させれば、コミットを共有しながら開発ができるというイメージが持てればよいです。
gitチーム開発の基本操作
チーム開発の基本となる動きはこれまでに説明したことだけです。
gitによるチーム開発の考え方の中心にあるのは
自分のほしいコミットを人のリポジトリからもらってくる
です。Subversionなどでは他人の入れた修正は必ず自分が取得しないといけませんでした。しかしgitでは自分の管理するリポジトリは自分だけのものであり、人からどうこうされることはありません。
このことは特に大きなチームになってくると重要になってきます。それぞれが独立して開発を進めることができるため、その他のいろいろな人の修正に影響されずに開発を進められます。必要であればコミットを取ってきますが、必要なければ取ってきません。
あとはこの操作を使ってどうコミットグラフを成長させていくか、ということになります。そしてここでもgitにはこれらの操作を楽にするための仕組みがあるので、それを用いながらコミットグラフおよびブランチを操作して開発を続けていきます。
もう少し具体的なシナリオで
ここからはもう少し具体的なシナリオに沿って、作業をしていきたいと思います。もし2人で作業できるようであれば2人で操作してみてください。一人で行う場合は一人二役で行ってください。(Aさん、Bさんとします。)
全体のシナリオはこうです。Aさんは一人で開発を進めていましたが、Bさんと一緒に作業をすることになりました。基本はAさんがメインで作業し、Bさんには手伝ってもらうという形です。Bさんはまだ作業に不慣れなので、作ったものはAさんが確認して、最終成果物に入れるかどうかを判断します。
A:
gitリポジトリを作成し適当なコミットを一つ作りましょう。(デフォルトのままmasterブランチで)
B:
Aさんのgitリポジトリをcloneしましょう。同一PCの場合、ディレクトリ指定でもいいですし、別PCの場合はA側にSSHサーバー立てることでcloneできます。
$ git clone /home/A/MyRepo ~/MyRepo // AさんのMyRepoリポジトリを自分のMyRepoにClone
または、
$ git clone ssh://username@server_name/home/A/MyRepo ~/MyRepo // ssh経由でClone
ファイルはコピーできれば良いので読み取り権限さえあればclone可能です。SSH経由の場合は相手のファイルにアクセスできるアカウントでログインしなくてはいけないことに注意してください。
A:
自分の作業を進めましょう。今回はmasterブランチでこのまま作業を進めていきます。適当な編集を入れてコミットを作成してください。
B:
Aさんの手伝いのための作業を進めます。Aさんが作っていない別のファイルを一つ作成してコミットしましょう。こちらもmasterブランチで作業しましょう
A:
Bさんが作成したコミットを取得しましょう。同一PCの場合はディレクトリ指定、別PCの場合はSSH経由で取得できます。
$ git fetch /home/B/MyRepo master
または
$ git fetch ssh://username@server_name/home/B/MyRepo master
先程の例ではコミットハッシュを使いましたが、今回はブランチを指定しています。このブランチmasterはBさんの方のmasterを意味しています。masterが指しているコミットを取ってこい、という命令になります。
ここで、現在のリポジトリの様子を図示しておきます。
図にFETCH_HEAD
というポインタが出てきました。先程の説明では省いてしまったのですが、これはgit fetch
をすると設定される一時的なポインタで直前にfetchしたコミットを指すように自動的にセットされるものです。
つづけて、いま取ってきたコミットの内容を確認しましょう。
$ git checkout -b B_Work FETCH_HEAD // 今取得したコミットにB_Workというブランチを作成してチェックアウト
fetchしたコミットにはブランチなどは勝手に作成されないので、checkoutしたい場合などはコミットハッシュを確認しないと行けないのですが、このFETCH_HEAD
のおかげでその手間を省けます。
もし内容に問題なければ、自分の作業とマージしましょう。
$ git checkout master
$ git merge B_Work // マージして
$ git branch -d B_Work // B_Workブランチは使わないので削除
ここまでのところでコミットグラフは以下のようになっています。
B:
Bさんは、さらにAさんの手伝いをするためにAさんの最新の状態を取得ます。
$ git fetch /home/A/MyRepo master // (SSHの場合も同様に)
ここでの注意点は2つです。1つ目は先ほどと同様、Bさんの方のリポジトリではまだB1
をmasterが指している点です。Aさんのmaster
を取得しても自分のリポジトリのブランチは移動しません。
もう一つは、Aさんのmaster
を取得すると、そのコミットに必要なすべてのコミットが取得されるという点です。BさんのリポジトリにはA2
,A3
が存在していません。そしてA側のmaster
の指すA3
を取得しようとしますが、そのためにはまずA2
が存在しないといけません。何故かと言うとA3
のコミットの中にはParentとしてA2
が記録されており、この存在が必須だからです。gitは取得しようとするコミットに必要なすべてのコミットをfetchで取得します。
Bさんがこの先、いま取ってきFETCH_HEADを元に作業をするか、はたまたmasterで作業を続けるのかはgitの上では自由です。後はチームで話し合ってどうするか決めます。おそらくはFETCH_HEADを起点に再度作業を続けるのが通常の流れかと思いますが、今回はここまでにしておきます。
二人でお互いのコミットグラフを同期しながら作業する場合には、もう少しいいやり方があるので、次回その紹介をします。今回は原理としてこのようにしていけば二人の人が同じコミットグラフを共有しながら開発が進められるということを理解してください。
リモートリポジトリ
これまでのシナリオではfetchをするときに毎回相手リポジトリのフルパス(またはURL)を入れて指定していましたがこれは面倒です。また、fetchする相手が複数になったときにも、それぞれのパスを覚えておくのも大変です。
gitには相手のリポジトリのパスに名前を付けて記憶させておくことができます。
Bさんのリポジトリ内で以下のコマンドを実行してみてください。
$ git remote -v
origin /home/A/MyRepo/ (fetch)
origin /home/A/MyRepo/ (push)
origin
という名前が/home/A/MyRepo
に紐付けられています(ディレクトリ指定でclone場合)。
git clone
は自動的にclone元のパスをorigin
という名前で記録しておいてくれます。
このようにgitで記録されているリポジトリをリモートリポジトリと呼びます。このように記録しておくことでこの名前を使ってfetchなどを実行することができます。つまりBさんがAさんのリポジトリからfetchしたいときは
$ git fetch origin master // /home/A/MyRepoの代わりにoriginを指定
とすれば良いことになります。
origin
という名前はclone元を表すのによく使われますが、これはたまたまgitがデフォルトで付けるからそうなっているだけです。origin
という名前そのものが特別な意味を持つわけではありません。origin
が常にclone元を指しているとも限りません。
一方、Aさんのリポジトリにはまだリモートリポジトリはありません。Bさんのリポジトリはよく使うことになると思うので、自分でリモートリポジトリとして追加することができます。
$ git remote add B_Repo /home/B/MyRepo //または
$ git remote add B_Repo ssh://username@server_name/home/B/MyRepo
これでB_Repo
という名前でリモートリポジトリを登録します。もちろんB_Repo
の部分は自由につけて構いません。
これで、Aさんもgit fetch B_Repo master
のようにしてBさんのリポジトリからコミットを取得できます。
また、リモートリポジトリに対して自分自身のリポジトリをローカルリポジトリと呼ぶことがあります。
課題
課題1: お互いにマージコミットをやりとり
今回は、他の人が作成したリポジトリをCloneしてお互いにコミットをやり取りする、ということをしていきたいと思います。(一人でやる場合は一人二役でやりましょう。)常に、お互いのコミットグラフがどうなっているかをイメージしながら作業を進めて下さい。
まず、AさんのリポジトリをクローンしてBさんと共有しましょう(すでに作ってあるものでよいです)。
masterブランチの地点から、AさんはBranchAで作業、BさんはBranchBで作業します。
それぞれいくつかの修正をしてコミットを2つ作成してください。
Bさんは、Aさんの作業内容を取得して、マージしましょう。(ここでできるマージコミットをM1とします)
AさんはBさんのマージした結果M1を取得して、再度そこから作業を始め、2つコミットを作成しましょう
BさんはM1から再度作業を始めて、コミットを2つ作成しましょう。
AさんはBさんのM1の先の作業内容を取得してマージしましょう。(ここでできるマージコミットをM2とします)
BさんAさんのマージした結果M2を取得しましょう
課題2: 全く関係のないリポジトリからコミットを取得
git fetch
は指定したリポジトリからコミットをコピーしてくることが分かりました。では、コピーしてくるコミットとして、今作業しているリポジトリとは全く関係のないリポジトリのコミットを指定した場合どうなるのでしょうか?
今回やったシナリオはすべてgit clone
してリポジトリをコピーしてから操作しているので、少なくとも2つのリポジトリの共通するコミットが存在していました。そうでない場合どうなるのでしょうか?
試しに以下のようにリポジトリ、コミットを自分の適当なリポジトリにfetchしてみてください。
$ git fetch https://github.com/JugglerShu/dotfiles master
どうなっているか分かりますか?
まとめ
今回は、どのようにリポジトリ間でコミットをやり取りするか、またそのときに必要になる概念について説明しました。
ここまでのことが分かれば、チーム開発で必要なことはすべてできます。今回も今まで同様、本質的には必要ないgit push
、git pull
などのチーム開発でよく使われるコマンドは使いませんでした。
次回はこれらを用いながら実際の開発シナリオで使う作業パターンを見ていきます。