12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

仕組みから理解するgit入門 第四回

Last updated at Posted at 2019-05-11

第三回はこちら

前回の課題の解説とポイント

課題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が指しているコミット内容がワーキングツリーに展開されます。

このとき、このリポジトリのコミットグラフは以下のような状態になっています。

image.png

上図のように、dotfilesリポジトリ内のmasterが指しているコミットをこちらのリポジトリ内に取り込みます。仮にdotfilesリポジトリが上図のように4つのコミットから構成されているとすると、masterを取り込むには、そのParent及びすべての先祖のコミットが必要になるので、C1C4まですべてこちらに取り込みます。両リポジトリ間に共通するコミットはないので、2つの独立したコミットグラフが人るのリポジトリ間に存在するような形になります。

このようなリポジトリの形は比較的特殊な形ではありますが、時としてこのように完全に独立したものを一つのリポジトリで管理するようなこともありますし、できます。

チーム開発その2

今回は前回に続いてチーム開発での操作についてやっていきます。

リモートリポジトリとローカルリポジトリでの同期、連携の続きです。前回はgit fetchを用いてコミットを取得しました。

今回はgit pushgit pullいうコマンドを主に見ていきますが、あとで説明するようにgit pullは使わなくて大丈夫なので、git pushだけ理解すれば大丈夫です。

git pushgit pullはGitHubを使うといきなり出てくるので、なんとなくサーバーにコミットをアップロードする、ダウンロードするというような意味で利用している人もいますが、実際に起きることはそれだけではありません。そのため、これらのコマンドを使った結果、よくわからないコミットグラフの状況にローカルリポジトリもリモートリポジトリもしてしまうという状況に陥る人がいるのではないかと思います。

これらのコマンドを適切に利用するにはコミットグラフとそれに関係するブランチがどう更新されるかを理解しておく必要があります。

git push

リポジトリ間のコミットのやりとりは、git fetchを用いて自分のリポジトリに必要なコミットを取り込むのが基本ですが、こちらからコミットを送信こともできます。それがgit pushです。もちろんこれをするには対象リポジトリへの書き込み権限がなくてはいけません。

通常のチーム開発では人のリポジトリへの書き込み権限があることはありません(あったら嫌ですよね)。
git pushを使うのは他人のリポジトリへの書き込みではなく、自分で管理する2つ以上のリポジトリ間でやり取りする場合、または全員で共有しているリポジトリへコミットを送信する場合です。GitHubを利用している場合もこれに当たります。

ただし、git pushgit fetchの逆の操作と考えるのはよくありません。
gitのhelpを読むとそれぞれの説明は以下のようになっています。

git-fetch - Download objects and refs from another repository
git-push  - Update remote refs along with associated objects

git fetchDownload objectsであるというのに対して、git pushUpdate 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している(HEADmasterを指している)とそれができないのでmasterではないものをcheckoutしている状態にするためです。
image.png

git cloneするとなぜかRepoB側ではmasterがcheckoutされた状態になるようです。理屈を考えると不思議な動作ですがそうなるようです。

次に、RepoBでコミットをmaster上に一つ作成します。

$ echo edit file1 > text1.txt
$ git commit -a

image.png

そして、このコミットをgit pushを用いてRepoBからRepoA側に送信します。

$ git push origin master:master         // RepoB内で実行

RepoBはRepoAからcloneしているので、リモートリポジトリoriginはRepoAを表しています。

image.png

originが送信先(push先)リポジトリを示すことは理解できると思います。その後の引数master:masterが少し特殊です。
:で区切られた前半は送信するべきこちら側のコミットを指定します。今回はmasterの指しているコミットを送信しようとしているのでmasterを指定しています。
後半のmasterは、送信先の更新するべきブランチを示しています。この指定によりリモートリポジトリのmasterが今転送したコミットを指すように更新されるということになります。
この部分がgit fetchとは大きく異なる部分です。git fetchはローカルもリモートもブランチが移動されることはありません。

このコマンドを言葉で表すなら

RepoBにあるmasterの指すコミットをRepoAに送信し、そのコミットを指すようにRepoAのmasterを更新せよ

となります。

先程、一度HEADmasterから外したのは、このときHEADがmasterを指しているとそれができずにエラーになってしまうためです。RepoA側でcheckoutしているmasterが知らない間に移動していたら困ってしまうので。

いま、更に追加で以下のコマンドを実行したらどうなるでしょうか?

$ git push origin master:new_branch

リモート側で更新するべきブランチをnew_branchにしていますが、そのようなブランチはRepoA内には存在しません。その場合は新しくブランチ作られます。

image.png

今回も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

image.png

$ git push origin my_branch:master

image.png

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`とは別方向にコミットを進める

image.png

この状態で、このブランチを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.

image.png

コマンドの意味合いとしては上図の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 pullgit pushの省略形

よくgitの使い方でgit pushgit 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リポジトリに対して書き込み権限を持った状態で開発を進めていきます。

image.png

中央にリポジトリを作成し、それを全員がcloneします。通常は中央にSSHサーバーやGITサーバー(gitプロトコルで接続できる)などでアクセスしcloneします。

各自が自分の担当分をローカルリポジトリ上で開発しコミット、pushすることで中央のコミットグラフを成長させていきます。

ただし、全員がmaster上で作業してコミットを進めていくとそれぞれのコミットがローカルリポジトリ上で別の方向に進んでいき、pushできない状態になってしまいます。

そのため、masterは常にひとつの方向に進めるように全員がルールを守る必要があります。

例えば以下のようにします。

git cloneして、別ブランチをmasterの位置で作成しcheckoutします。

image.png

別ブランチ上で必要な修正などを入れコミットを作成します。それと同時にリモートリポジトリでは誰かがmasterブランチを更新したという状況を考えます。

image.png

C2をリモートリポジトリにpushしたいですがこのままではできません。そのためまずリモートから最新のmasterを取得し、ローカルのmasterを更新します。

image.png

C2の変更をmasterに取り込みたいのでローカルでC2masterにマージします。

image.png

最後にできあがったmasterをリモートにpushします。

image.png

(左右でグラフの形が異なるのに意味はありません。論理的なつながりが同じであれば同じコミットグラフになります。)

このようにするとmasterは常に一定方向に進むことになります。コミットグラフの枝分かれは常にローカルブランチでのみ発生します。

現実的な開発ではこの方法にも問題点はたくさん存在します。たとえば、中央リポジトリのmasterを誰でも更新できます。このルールは守ることが前提ですが守られないこともあるかもしれません。誰かの作成したコミットにバグが含まれていて、それをmasterが指すこともあるかもしれません。誰かが間違えたコマンドを実行すれば、たちまち全員の開発に影響を与えます。

そのため、この運用方法は少ない人数でそれぞれしっかりコミュニケーションが取れる状態でないと運用が難しいです。

このパターンを取りながらも、もう少し手軽にこの問題点を解決する方法としては、ブランチをマージする人を固定しておくという方法があります。開発を進める人はマージはせずに、常に自分の作成したブランチをリモートリポジトリ上にPushするようにします。それを見たマージ担当が者必要に応じてそれをmasterにマージしていきます。このようにすると、masterが先に進むのはマージ担当者がブランチをマージしたときのみになります。加えてブランチの名前付けの規則なども導入しておけば比較的混乱なく作業を進められると思います。

パターン2 : それぞれが独立 階層型に管理

パターン1だと、中央のリポジトリに誰でも自由に変更を加えられるという問題があるので、それを改善するパターンを考えてみます。また、より大きなプロジェクトに対応できるように全体の管理を階層化してみます。
ここではパターン1に加えて2つのことを同時に変更しているので混乱しないようにしてください。どちらか一つだけでも運用に取り込むことはできます。

例として、開発全体を2つのチームに分け、それぞれが別々の部分を開発し、それを最終的に統合して一つのプロダクトを作成するような体制を考えます。それぞれのチームは3人おり、うち1人がリーダです。さら、開発全体をまとめる人が1人いるような体制を考えます。

この組織を表現するようにリポジトリを用意し、上位のリポジトリ管理者が下位リポジトリからコミットを吸い上げる(fetchする)形で運用していきます。

image.png

下位リポジトリは定期的に上位リポジトリからコミットをfetchして開発してきます。重要な点は、それぞれのリポジトリにはそのリポジトリの所有者しか変更権限がないということでです。git pushが存在しません。

全体をスムーズに運営するために以下のようなルールを決めておきます。

  • 下位リポジトリは上位リポジトリをcloneする
  • 下位リポジトリではmasterにはコミットを作成しない
  • 下位リポジトリでは定期的に上位masterをfetchしてくる
  • 下位リポジトリでコミットを進める場合にはmasterから新ブランチを作成して進める
  • 最終的にマージして良いコミットが出来上がったら上位リポジトリ管理者に伝えて上位リポジトリでマージしてもらう

この流れをリーダー(上位)、と開発者(下位)のペアで見てみます。

下位リポジトリは上位リポジトリをcloneしますし、ブランチを作成して作業を進めます。

image.png

下位リポジトリでコミットを作成します。同時に上位リポジトリには別の下位リポジトリからの修正が取り込まれたという状況を考えます。

image.png

ここからが先程の共有パターンと少し変わるところです。下位リポジトリを持つ開発者はリーダーに対してC2が完成したので自分のbranchmasterに取り込んでほしいと伝えます。そして、まずリーダーはそれをfetchします。

image.png

リーダーは自分のリポジトリの取り込んだC2の内容を確認しmasterに取り込んで良いかチェックします。大丈夫そうならマージします。

image.png

このようにすることで、上位リポジトリのmasterに入るコミットは常に上位の管理者が管理をすることになります。仮に下位リポジトリで誤ってmasterにコミットを作成したとしても、最上位リポジトリ管理者がそれをfetchしたりmergeしたりしない限り全体への影響はありません。

下位開発者がさらに開発を進めるには再度上位リポジトリのmasterをfetchし、そこにブランチを作成してから進めます。

image.png

この方法にもいくつか問題はあります。

1つ目は、それぞれのリポジトリがサーバーとして動作していないといけないという点です。通常は別マシン上でそれぞれが開発するはずなので、それぞれのマシンにSSHサーバーなりを立てて、上位や下位から通信できる状態にしておかないといけません。

もう一つは、上位にfetchすべきブランチを伝えなくては行けない点です。これはどうしようもないのですが、もう少しスマートな方法が欲しいものです。

そして、これらの問題を解決するものこそがGitHubになります。

次回はGitHubを用いて、このチーム開発の進め方を見ていきたいと思います。

課題

git pushの使い方を理解するために二人でペアになり、一つのリポジトリを共有して開発を進めてみましょう。
二人をAさん、Bさんとします。一人で行う場合には一人二役で行ってください。

  1. 二人で共有するリポジトリを作成しましょう。このリポジトリはAさん、Bさん、二人から書き込みができる必要があります。
    SSHサーバー上にリポジトリを作成する場合、二人で共通のSSHアカウントを利用することで一つのリポジトリに二人共書き込める状況を作れます。
    (ここでは簡単に、Aさんのホームディレクトリ内に適当なリポジトリを作成してAさんのアカウントを共有する形で問題ありません。)

    リポジトリを作成するには以下のコマンドを利用します。

$ mkdir Project1
$ cd Project1
$ git init --bare   // <----- オプションに注意

ここで作成したリポジトリを共有リポジトリと呼ぶことにします。

git init --bare が初めて見るオプションかと思います。これはこのリポジトリで基本的に直接ワーキングツリーを使って編集を行ったりしない場合に使います。今回はこのリポジトリはAさん、Bさんにcloneされて利用されるのでこのようにしていします。(このようにするとこのリポジトリいおいてはどのブランチもチェックアウトされていない状態が作れます。)

  1. 次に、Aさん、Bさんともにそのリポジトリをcloneします。共有リポジトリと合わせて3つのリポジトリが出来上がります。

ここからは、あまり具体的にgitの操作を指示しません。代わりに実現したい開発上の作業を示しますので、それを実現してください。方法はたくさんあるので自由に実現してください。AさんとBさんのリポジトリ間はfetchやpushはしません。すべて共有リポジトリ経由でやり取りしてください。

  1. Aさんはリポジトリに最初のファイルtextA.txtを作成します。これはBさんとも共有するべき内容なので共有リポジトリにpushします。そしてBさんに続きの開発を任せました。

  2. Bさんは、Aさんの作成したtextA.txtに修正を入れる必要があるので、その修正を入れ共有リポジトリにpushします。

  3. ここから、Aさん、Bさんは別々の作業に入ります。Aさんは、最新のtextA.txtに修正を入れていきます。一方Bさんは、textBを作成して自分の開発を進めます。

  4. 二人の作業内容をマージし、共有リポジトリにpushしてください。共有リポジトリ上のmasterブランチがそのコミットを指すようにしてください。やり方はたくさんあります。

12
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?