前回の課題の解説とポイント
課題1
Bさん
Aさんの持っているリポジトリをclone
> git clone /path/to/RepoA RepoB // Aさんのリポジトリをclone
> cd RepoB
Aさん、Bさん共に
ブランチを作成して適当なコミットを2つ
> git checkout -b BranchA // BさんならBranchB
> echo new file > file1.txt
> git add file1.txt
> git commit
> echo another file > file2.txt
> git add file2.txt
> git commit
Bさん
Aさんの作業内容を取得してマージ
> git fetch origin BranchA // BranchAを取ってくる。FETCH_HEADがそれを指す
> git merge FETCH_HEAD // BranchB上で実行。 BranchBがM1を指す
Aさん
Bさんの作業内容を取得してマージ。そこから更に2つコミットを作成。
> git fetch /path/to/RepoB BranchB // RepoBのBranchBのコミットを取得
> git checkout FETCH_HEAD -b BranchA2 // M1をBranchA2ブランチを作成してcheckout
> echo work in BranchA2 > file1.txt
> git commit -a
> echo another work in BranchA2 > file2.txt
> git commit -a
Bさん
同様にM1の先にコミットを作成。BranchBがM1を指しているのでそのまま作業、コミットを2つ作成。
> echo work in BranchB > WorkInB.txt
> git add WorkInB.txt
> git commit
> echo another work in BranchB > WorkInB.txt
> git commit -a
Aさん
Bさんの作業内容をマージ
> git fetch /Path/to/RepoB BranchB
> git merge FETCH_HEAD // BranchA2上で実行。BranchA2がM2を指す
Bさん
Aさんの作業内容を取得
> git fetch origin BranchA2
> git checkout FETCH_HEAD -b BranchB2 // M2から作業を進めたい場合はブランチを作成してcheckout
課題2
実際にやってみると以下のようになります。
$ git fetch https://github.com/JugglerShu/dotfiles master
warning: no common commits
remote: Enumerating objects: 117, done.
remote: Total 117 (delta 0), reused 0 (delta 0), pack-reused 117
Receiving objects: 100% (117/117), 77.19 KiB | 264.00 KiB/s, done.
Resolving deltas: 100% (49/49), done.
From https://github.com/JugglerShu/dotfiles
* branch master -> FETCH_HEAD
なんか、動作しているように見えますね。gitからするとコミットは、コミットでしかありません。fetch
によってあるコミットを取ってこいと言われれば取ってきます。今回も取ってきました。ここで、
git checkout FETCH_HEAD
とすれば、実際にhttps://github.com/JugglerShu/dotfiles
のmasterが指しているコミット内容がワーキングツリーに展開されます。
このとき、このリポジトリのコミットグラフは以下のような状態になっています。
上図のように、dotfiles
リポジトリ内のmasterが指しているコミットをこちらのリポジトリ内に取り込みます。仮にdotfiles
リポジトリが上図のように4つのコミットから構成されているとすると、master
を取り込むには、そのParent及びすべての先祖のコミットが必要になるので、C1
〜C4
まですべてこちらに取り込みます。両リポジトリ間に共通するコミットはないので、2つの独立したコミットグラフが人るのリポジトリ間に存在するような形になります。
このようなリポジトリの形は比較的特殊な形ではありますが、時としてこのように完全に独立したものを一つのリポジトリで管理するようなこともありますし、できます。
チーム開発その2
今回は前回に続いてチーム開発での操作についてやっていきます。
リモートリポジトリとローカルリポジトリでの同期、連携の続きです。前回はgit fetch
を用いてコミットを取得しました。
今回はgit push
とgit pull
いうコマンドを主に見ていきますが、あとで説明するようにgit pull
は使わなくて大丈夫なので、git push
だけ理解すれば大丈夫です。
git push
やgit pull
はGitHubを使うといきなり出てくるので、なんとなくサーバーにコミットをアップロードする、ダウンロードするというような意味で利用している人もいますが、実際に起きることはそれだけではありません。そのため、これらのコマンドを使った結果、よくわからないコミットグラフの状況にローカルリポジトリもリモートリポジトリもしてしまうという状況に陥る人がいるのではないかと思います。
これらのコマンドを適切に利用するにはコミットグラフとそれに関係するブランチがどう更新されるかを理解しておく必要があります。
git push
リポジトリ間のコミットのやりとりは、git fetch
を用いて自分のリポジトリに必要なコミットを取り込むのが基本ですが、こちらからコミットを送信こともできます。それがgit push
です。もちろんこれをするには対象リポジトリへの書き込み権限がなくてはいけません。
通常のチーム開発では人のリポジトリへの書き込み権限があることはありません(あったら嫌ですよね)。
git push
を使うのは他人のリポジトリへの書き込みではなく、自分で管理する2つ以上のリポジトリ間でやり取りする場合、または全員で共有しているリポジトリへコミットを送信する場合です。GitHubを利用している場合もこれに当たります。
ただし、git push
をgit fetch
の逆の操作と考えるのはよくありません。
gitのhelpを読むとそれぞれの説明は以下のようになっています。
git-fetch - Download objects and refs from another repository
git-push - Update remote refs along with associated objects
git fetch
がDownload objectsであるというのに対して、git push
はUpdate remote refsです。ここでrefsというのはブランチのことだと思ってもらえればよいです。つまりリモートリポジトリに存在するブランチを更新するのがgit push
です。
git fetch
ではどちらのリポジトリもブランチが更新されることはありませんでしたが、git push
はそうではありません。
具体的な操作を通して動作を見ていきたいと思います。
新しいリポジトリ(RepoA)を一つ作成し、それをクローン(RepoB)し、RepoBをローカルリポジトリ、RepoAをリモートリポジトリとします。(クローンしたRepoBがローカルであることに注意してください。)
$ mkdir RepoA
$ cd RepoA
$ git init
$ echo file1 > text1.txt
$ git add text1.txt
$ git commit // RepoAにコミットを一つ作成
$ git checkout --detach HEAD // HEADにあるコミットを直接checkout (HEADはmasterではなくコミットを直接指す状態に)
$ cd ..
$ git clone RepoA RepoB // RepoBをローカルとして利用
git checkout --detach HEAD
の部分が特殊です。これはこの後このRepoAに対してgit push
しようとしたときに、masterをcheckoutしている(HEAD
がmaster
を指している)とそれができないのでmasterではないものをcheckoutしている状態にするためです。
git cloneするとなぜかRepoB側ではmasterがcheckoutされた状態になるようです。理屈を考えると不思議な動作ですがそうなるようです。
次に、RepoBでコミットをmaster上に一つ作成します。
$ echo edit file1 > text1.txt
$ git commit -a
そして、このコミットをgit push
を用いてRepoBからRepoA側に送信します。
$ git push origin master:master // RepoB内で実行
RepoBはRepoAからcloneしているので、リモートリポジトリorigin
はRepoAを表しています。
origin
が送信先(push先)リポジトリを示すことは理解できると思います。その後の引数master:master
が少し特殊です。
:
で区切られた前半は送信するべきこちら側のコミットを指定します。今回はmaster
の指しているコミットを送信しようとしているのでmaster
を指定しています。
後半のmaster
は、送信先の更新するべきブランチを示しています。この指定によりリモートリポジトリのmaster
が今転送したコミットを指すように更新されるということになります。
この部分がgit fetch
とは大きく異なる部分です。git fetch
はローカルもリモートもブランチが移動されることはありません。
このコマンドを言葉で表すなら
RepoBにあるmaster
の指すコミットをRepoAに送信し、そのコミットを指すようにRepoAのmaster
を更新せよ
となります。
先程、一度HEAD
をmaster
から外したのは、このときHEADがmaster
を指しているとそれができずにエラーになってしまうためです。RepoA側でcheckoutしているmaster
が知らない間に移動していたら困ってしまうので。
いま、更に追加で以下のコマンドを実行したらどうなるでしょうか?
$ git push origin master:new_branch
リモート側で更新するべきブランチをnew_branch
にしていますが、そのようなブランチはRepoA内には存在しません。その場合は新しくブランチ作られます。
今回もgitはまず、RepoB内でmaster
の指すコミットをまずRepoAに送信しますが、すでに同じコミットがRepoA内に存在するのでコミットの転送は実際には起きません。そして、RepoA内でnew_branch
がそのコミットを指すように作られます。
逆に、RepoB内でmasterとは全く関係のないブランチを作成して、それをRepoAのmasterが指すように更新することもできます。
$ git checkout -b my_branch
$ echo new file > text2.txt
$ git add text2.txt
$ git commit
$ git push origin my_branch:master
git push
の動作はこのようになっているので、実行する際には今転送しようとしているこちらのコミットがリモート側のどのブランチで示されるべきかを考える必要があります。
ただし、現実的なシナリオでは両リポジトリ間で同じブランチ名を利用することが多いです。そのためコマンドとしては
$ git push RepoA master:master
のように:
の両側が同一になることが多いです。この場合省略して
$ git push RepoA master // == master:master
と書くことができます。
(この説明は正確ではなく、省略した場合に:
の両側が異なるように解釈される場合もありますが特殊な場合なので今は気にしなくて良いと思います。)
Pushできないパターン
さて、このようにgit push
はリモートリポジトリのブランチの位置を移動させるので、ある意味では危険です。素直に考えれば突然リモートのブランチが今までの履歴の流れとは別の場所を指し示すようなことにもなりかねません。
以下のようなシナリオを考えます。RepoBでmasterの流れとは全く別の流れを作って、それをRepoAのmasterにpushしてみます。
$ git checkout HEAD~2 -b my_branch2 // 最初のコミットをmy_branch2を作成しながらcheckout
$ echo another file > text3.txt
$ git add text3.txt
$ git commit // `master`とは別方向にコミットを進める
この状態で、このブランチをpushして、RepoAのmasterを更新してみます。さて、どうなるでしょうか?
$ git push origin my_branch2:master
To /home/shu/develop/RepoA
! [rejected] my_branch2 -> master (non-fast-forward)
error: failed to push some refs to '/home/shu/develop/RepoA'
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes
hint: (e.g. 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
コマンドの意味合いとしては上図のRepoA内に点線部分のC4
のコミットができて、それをmaster
が指すように更新されるはずです。
しかし、実際にはこのような操作はgitがエラーとして実行してくれません。RepoA内のmaster
は、現在の位置から子供の方向にしか移動させることができないようになっています。このようにして、ある程度RepoA内での状態がおかしな状況になることを防いでくれています。
時としてどうしても今の流れとは別の場所にブランチを持っていきたい場合がありますが、その場合は-f
オプションを付けることで可能です。
$ git push -f origin my_branch2:master
これによってもともとイメージしたような状態にRepoAがなります。通常の流れとは別の場所にブランチを持っていくことになるので、注意して使いましょう。(特にこの後に説明するリポジトリを共有する形で開発を進めている場合には基本やるべきではありません。)
git pull
git fetch
と似たようなコマンドにgit pull
があります。
結論から言えば、git pull
は使わなくていいです。むしろ、使わないでください。
使う場合は以下をしっかり読んで完全に理解してから使ってください。使わない場合は読まなくていいです。
一言で説明すると
git pull
= git fetch
+ git merge FETCH_HEAD
です。
なので本来必要のないコマンドです。ただよく行うオペレーションなのでそのためのコマンドが用意されているというだけです。このコマンドは注意が必要で、それは後半のgit merge
の部分です。勝手にマージが実行されます。git merge
はマージコミットを作成します。つまり、git pull
は新たなコミットを作ります(場合によっては作らないこともありますが。)。さらに、現在作業しているブランチもその新たなコミットに移動させます。git merge
は現在checkoutしているブランチ上で実行され、そこにマージされてしまうのです。
git pull
はfetchしようとしているコミットが確実に自分の現在作業中のブランチにマージされても問題ないという自信のあるときしか使えません。筆者の場合しょっちゅうfetchすべきブランチを間違えたりするので、まずこのコマンドは使いません。fetchしてから確かに自分が思っていたコミットを取得したと確認してからmergeします。
git pull
とgit push
の省略形
よくgitの使い方でgit push
やgit pull
を引数無しで実行している例を見かけるかと思います。
しかし、これらはリモートブランチ,アップストリーム,トラッキングブランチといういくつかのgitの機能を理解しておかないと正しく動きを把握できません。
ですので、個人的には省略形を使うのはおすすめしません。省略せずにきちんとコマンドを使ったほうが正しく使えます。
使うのであればそれらの概念を理解してからにしたほうが良いと思います。コマンドの引数が省略されている以上、省略されている部分がgitによって補完されるわけですが、それらが何になるのかが把握できないと思わぬ状態に陥ります。
この部分を説明するとかなり長くなるのと、知らなくてもなんとかなるのでここでは説明しません。
興味のある方は以下を見てみてください。
https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%E6%A9%9F%E8%83%BD-%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81
リモートブランチぐらいは知っておくと便利かもしれません。
リポジトリの連携パターン
ここまでは2つのリポジトリを用いて一般的なコミットのやり取り、ブランチの更新の方法を見てきました。
あとはこれを応用して複数人で開発を進める場合にもお互いにコミットを交換したりブランチを更新するなどして進めればよいです。
一方でgitはある一定の仕組みを提供しているだけで自由度が高いためそうは言われてもどうしてよいのか分からない部分もあると思います。
きちんとgitの仕組みを理解していればどのような状況でも対応できるのですが、一定のパターンがあった方が最初は楽に進められるのは確かです。
ここでは代表的な2つの複数のリポジトリの連携パターンを紹介します。実際はここに示したパターンで使われているいくつかの要素を組み合わせて実際のプロジェクトの運用方法を構築することになると思います。
パターン1 : 一つのリポジトリをみんなで共有
これはどちらかというとSubversionなどの中央管理型の仕組みに近い形でgitを運用する場合のパターンです。すべての人が一つのgitリポジトリに対して書き込み権限を持った状態で開発を進めていきます。
中央にリポジトリを作成し、それを全員がcloneします。通常は中央にSSHサーバーやGITサーバー(gitプロトコルで接続できる)などでアクセスしcloneします。
各自が自分の担当分をローカルリポジトリ上で開発しコミット、pushすることで中央のコミットグラフを成長させていきます。
ただし、全員がmaster上で作業してコミットを進めていくとそれぞれのコミットがローカルリポジトリ上で別の方向に進んでいき、pushできない状態になってしまいます。
そのため、masterは常にひとつの方向に進めるように全員がルールを守る必要があります。
例えば以下のようにします。
git clone
して、別ブランチをmasterの位置で作成しcheckoutします。
別ブランチ上で必要な修正などを入れコミットを作成します。それと同時にリモートリポジトリでは誰かがmaster
ブランチを更新したという状況を考えます。
C2
をリモートリポジトリにpushしたいですがこのままではできません。そのためまずリモートから最新のmaster
を取得し、ローカルのmaster
を更新します。
C2
の変更をmaster
に取り込みたいのでローカルでC2
をmaster
にマージします。
最後にできあがったmaster
をリモートにpushします。
(左右でグラフの形が異なるのに意味はありません。論理的なつながりが同じであれば同じコミットグラフになります。)
このようにするとmasterは常に一定方向に進むことになります。コミットグラフの枝分かれは常にローカルブランチでのみ発生します。
現実的な開発ではこの方法にも問題点はたくさん存在します。たとえば、中央リポジトリのmasterを誰でも更新できます。このルールは守ることが前提ですが守られないこともあるかもしれません。誰かの作成したコミットにバグが含まれていて、それをmasterが指すこともあるかもしれません。誰かが間違えたコマンドを実行すれば、たちまち全員の開発に影響を与えます。
そのため、この運用方法は少ない人数でそれぞれしっかりコミュニケーションが取れる状態でないと運用が難しいです。
このパターンを取りながらも、もう少し手軽にこの問題点を解決する方法としては、ブランチをマージする人を固定しておくという方法があります。開発を進める人はマージはせずに、常に自分の作成したブランチをリモートリポジトリ上にPushするようにします。それを見たマージ担当が者必要に応じてそれをmasterにマージしていきます。このようにすると、masterが先に進むのはマージ担当者がブランチをマージしたときのみになります。加えてブランチの名前付けの規則なども導入しておけば比較的混乱なく作業を進められると思います。
パターン2 : それぞれが独立 階層型に管理
パターン1だと、中央のリポジトリに誰でも自由に変更を加えられるという問題があるので、それを改善するパターンを考えてみます。また、より大きなプロジェクトに対応できるように全体の管理を階層化してみます。
ここではパターン1に加えて2つのことを同時に変更しているので混乱しないようにしてください。どちらか一つだけでも運用に取り込むことはできます。
例として、開発全体を2つのチームに分け、それぞれが別々の部分を開発し、それを最終的に統合して一つのプロダクトを作成するような体制を考えます。それぞれのチームは3人おり、うち1人がリーダです。さら、開発全体をまとめる人が1人いるような体制を考えます。
この組織を表現するようにリポジトリを用意し、上位のリポジトリ管理者が下位リポジトリからコミットを吸い上げる(fetchする)形で運用していきます。
下位リポジトリは定期的に上位リポジトリからコミットをfetchして開発してきます。重要な点は、それぞれのリポジトリにはそのリポジトリの所有者しか変更権限がないということでです。git push
が存在しません。
全体をスムーズに運営するために以下のようなルールを決めておきます。
- 下位リポジトリは上位リポジトリをcloneする
- 下位リポジトリではmasterにはコミットを作成しない
- 下位リポジトリでは定期的に上位masterをfetchしてくる
- 下位リポジトリでコミットを進める場合にはmasterから新ブランチを作成して進める
- 最終的にマージして良いコミットが出来上がったら上位リポジトリ管理者に伝えて上位リポジトリでマージしてもらう
この流れをリーダー(上位)、と開発者(下位)のペアで見てみます。
下位リポジトリは上位リポジトリをcloneしますし、ブランチを作成して作業を進めます。
下位リポジトリでコミットを作成します。同時に上位リポジトリには別の下位リポジトリからの修正が取り込まれたという状況を考えます。
ここからが先程の共有パターンと少し変わるところです。下位リポジトリを持つ開発者はリーダーに対してC2
が完成したので自分のbranch
をmaster
に取り込んでほしいと伝えます。そして、まずリーダーはそれをfetchします。
リーダーは自分のリポジトリの取り込んだC2
の内容を確認しmaster
に取り込んで良いかチェックします。大丈夫そうならマージします。
このようにすることで、上位リポジトリのmasterに入るコミットは常に上位の管理者が管理をすることになります。仮に下位リポジトリで誤ってmasterにコミットを作成したとしても、最上位リポジトリ管理者がそれをfetchしたりmergeしたりしない限り全体への影響はありません。
下位開発者がさらに開発を進めるには再度上位リポジトリのmaster
をfetchし、そこにブランチを作成してから進めます。
この方法にもいくつか問題はあります。
1つ目は、それぞれのリポジトリがサーバーとして動作していないといけないという点です。通常は別マシン上でそれぞれが開発するはずなので、それぞれのマシンにSSHサーバーなりを立てて、上位や下位から通信できる状態にしておかないといけません。
もう一つは、上位にfetchすべきブランチを伝えなくては行けない点です。これはどうしようもないのですが、もう少しスマートな方法が欲しいものです。
そして、これらの問題を解決するものこそがGitHubになります。
次回はGitHubを用いて、このチーム開発の進め方を見ていきたいと思います。
課題
git push
の使い方を理解するために二人でペアになり、一つのリポジトリを共有して開発を進めてみましょう。
二人をAさん、Bさんとします。一人で行う場合には一人二役で行ってください。
-
二人で共有するリポジトリを作成しましょう。このリポジトリはAさん、Bさん、二人から書き込みができる必要があります。
SSHサーバー上にリポジトリを作成する場合、二人で共通のSSHアカウントを利用することで一つのリポジトリに二人共書き込める状況を作れます。
(ここでは簡単に、Aさんのホームディレクトリ内に適当なリポジトリを作成してAさんのアカウントを共有する形で問題ありません。)リポジトリを作成するには以下のコマンドを利用します。
$ mkdir Project1
$ cd Project1
$ git init --bare // <----- オプションに注意
ここで作成したリポジトリを共有リポジトリと呼ぶことにします。
git init --bare
が初めて見るオプションかと思います。これはこのリポジトリで基本的に直接ワーキングツリーを使って編集を行ったりしない場合に使います。今回はこのリポジトリはAさん、Bさんにcloneされて利用されるのでこのようにしていします。(このようにするとこのリポジトリいおいてはどのブランチもチェックアウトされていない状態が作れます。)
- 次に、Aさん、Bさんともにそのリポジトリをcloneします。共有リポジトリと合わせて3つのリポジトリが出来上がります。
ここからは、あまり具体的にgitの操作を指示しません。代わりに実現したい開発上の作業を示しますので、それを実現してください。方法はたくさんあるので自由に実現してください。AさんとBさんのリポジトリ間はfetchやpushはしません。すべて共有リポジトリ経由でやり取りしてください。
-
Aさんはリポジトリに最初のファイルtextA.txtを作成します。これはBさんとも共有するべき内容なので共有リポジトリにpushします。そしてBさんに続きの開発を任せました。
-
Bさんは、Aさんの作成したtextA.txtに修正を入れる必要があるので、その修正を入れ共有リポジトリにpushします。
-
ここから、Aさん、Bさんは別々の作業に入ります。Aさんは、最新のtextA.txtに修正を入れていきます。一方Bさんは、textBを作成して自分の開発を進めます。
-
二人の作業内容をマージし、共有リポジトリにpushしてください。共有リポジトリ上のmasterブランチがそのコミットを指すようにしてください。やり方はたくさんあります。