##概要
プログラマー1年目の自分が辿り着いたGitまとめ その1
プログラマー1年目の自分が辿り着いたGitまとめ その2
の続きです。
今回は3番目の環境『複数人での開発(ブランチはmasterのみ)』に遷移した前提で進めます。
##clone, pull
Aさんが作成しているアプリの開発に、Bさんも参画することになりました。
AさんとBさんはそれぞれアプリ内の別々の機能を開発するとします。
####clone - リモートリポジトリをコピーしたローカルリポジトリを作成する
リポジトリを新しく作成する時はgit init
ですが、既にGitHubなどに上がっているリモートリポジトリを自身の環境にローカルリポジトリとして作成する場合はこのコマンド。
オープンソースのライブラリとかもこれで手元に持ってこれるので、使う機会は多い。
BさんはまずAさんがGitHubにプッシュしているリポジトリをcloneする。
git clone [URL]
の形式で問題ありませんが、前回同様プライベートリポジトリだった場合はURLにGitHubのユーザー名を入れて、パスワード認証を通すことが必要になるので注意。
→ プライベートリポジトリに参画するにはGitHubのContributor設定で登録してもらいましょう。
そうして作成された自身のローカルリポジトリ内で、Aさんとは別の箇所の開発に取り掛かります。
####pull - リモートリポジトリの変更(コミット)をローカルに取り込む
コミットをリモートリポジトリに送るのがpushなのに対し、pullはその逆(と今は覚えておけばいいと思います)。
Bさんが別機能を完成させ、その分のコミットをリモートリポジトリにgit push
する。Aさんはそれを確認してgit pull
をする。
これによって、AさんのローカルリポジトリにBさんが開発した機能がコミットグラフに履歴が追加される形で取り込まれます。
因みにpushの時と同じくgit pull
も追跡対象になっているリモートブランチを自動的に引っ張ってくるので、まだ一度もpush及びpullしてない場合などはgit pull origin master
のように具体的にリモートブランチを指定する必要があります。
→ これもpush時と同様の話ですが、くれぐれもmasterブランチでgit pull origin develop
とかしてしまわないように気を付けましょう……
add - commitが**『バージョン管理』としての1サイクルと考えるならば
push - pullは『分散型管理』としての1サイクル**とみなすことが出来る。
ローカルでは前者のサイクルを回してコミットを重ねていき
後者のサイクルを回すことで、共同開発者とのコミットの共有を進めていくというイメージです!
##競合(コンフリクト)の解決, stash
ここでGitのややこしさの一因、競合(コンフリクト)について。
競合とはその名の通り、"変更同士がぶつかりあってしまうこと"です。
#####例
Aさんが何かしらの修正を行い、自身のローカルリポジトリにてコミットする。
Bさんも実は同じ箇所を修正していて、自身のローカルリポジトリにてコミットしていました。
ここでAさんがその変更をリモートリポジトリにpushしてしまいます。
BさんはAさんの変更を取り込むため、リモートリポジトリをpullする。
→ するとBさんのローカルリポジトリにて競合(コンフリクト)が発生し、pullが完了しない。
競合が起きた場合、お互いの変更を取り込んだ上で、重複して編集してしまった箇所には競合マーカーがつきます。
<<<<<<< HEAD
ローカルリポジトリでの変更(HEADは現在のコミットを指す)
etc...
============
リモートリポジトリでの変更
etc...
>>>>>>> develop(ここにはpullしようとしているブランチの名前が入ります)
両方の変更が上記のように区切られて残っているので、優先すべき方を残して編集してやる。
競合の解決が終わったら、改めてコミットし直してやることでpullが完了する。
因みに勘違いしやすいですが競合は別にエラーじゃないので、競合していなかった箇所については既に取り込みが完了しています。
なので競合を解決後に再びgit pull
しなきゃいけない、ってことはありません。
ただ競合マーカーを消して競合を解決し終わったという編集は、上述の通り『競合を解決しpullを完了した』コミットとして残しておくのが普通。
他の変更点と混ぜてコミットするのもバージョン管理的に良くないので、競合解決のコミットを行った段階でpullが完了した、と捉えるようにするのが良いと思います。
####競合を出来るだけ回避するには
上記の競合の例ですが、場合によっては簡単に回避出来ることもあります。
方法はシンプルで、先程説明したpush-pullのサイクルを意識することです。
上の例ではAさんが既に変更をpushしていて、Bさんはリモートに反映されたその変更分のコミットをpullしようとしている、という状況でした。
Aさんのローカル
(コミットa)--(コミットb)--(変更コミットc)--(変更コミットd)
↓ コミットc,dがpushされた
リモートリポジトリ
(コミットa)--(コミットb)--(コミットc)--(コミットd)
この時競合が起きたのは、Bさんが既に同様の箇所に変更をコミットしていたからです。
リモートリポジトリ
(コミットa)--(コミットb)--(コミットc)--(コミットd)
↓ 既にe,fがあるので、c,dと競合してしまう
Bさんのローカル
(コミットa)--(コミットb)--(変更コミットe)--(変更コミットf)
競合が起きること自体は当たり前に思えますが、この時注目して欲しいのは、pullしようとしているリモートリポジトリと、Bさんのローカルリポジトリにおいてコミットbに繋がっているコミットがそれぞれ異なっているという点。
コミットというのは『自身の一つ前のコミット』を情報として保持しています。
そのため今回のように、コミットcもコミットeも共に『コミットbに続くコミット』として作成された場合は、pullした際にそれらが並列なコミットとして扱われます。
こうした並列なコミットは、例えばちょっとした空行の削除などでも同様の箇所に差異があれば、同じタイミングで編集した箇所だと認識され、結果としてはそれは競合として捉えられてしまうのです。
push-pullのサイクルを意識することで、並列なコミットを極力減らすことが可能になります。
具体的には、Aさんが変更をpushしたというタイミングで、Bさんは自身の変更をコミットする前にリモートをpullするということです。
リモートリポジトリ(Aさんの変更がpush済)
(コミットa)--(コミットb)--(コミットc)--(コミットd)
↓ e,fをコミットする前にpullする
Bさんのローカル
(コミットa)--(コミットb)--(コミットc)--(コミットd)
↓ その後e,fをコミット
Bさんのローカル
(コミットa)--(コミットb)--(コミットc)--(コミットd)--(変更コミットe)--(変更コミットf)
↑ 先にc,dを取り込んでいるので、eはdに続くコミットになる!
元々あったa,bにまずgit pull
でAさんが追加したコミットc,dを繋げ、その後に自身のコミットe,fを繋ぐ。
こうすることで並列なコミットが無くなるため、上述の空行削除みたいな潜在的競合を失くすことが出来ます。
- 誰かがpushしたら、それに合わせて自分もpull
- pullする前にリモートの最新コミットとローカルの最新コミットを見比べる
この2つを意識すると、不意の競合を極力避けることが出来ると思います。
競合は100%避けられるようなものではないので、『起きそうなタイミング』や『起きる条件』を理解しておくことの方が重要になってきます。
####stash - 現在の変更を一時退避する
改めて上記の例について考えてみますが、pullしてから自身の変更をコミットするとなると、Bさんは既に編集済みの箇所やコミットしてしまった変更を巻き戻さなければならないように思えます。
しかし、かと言って折角の変更をgit reset --hard
してしまう必要はありません。
このように『後で復活させたい巻き戻し』の時はgit stash
が便利です。
#####一時退避
git stash
を行うと、まだコミットしていない変更をデータとして別の箇所に格納します。
この時git status
を併用すると分かりやすいですが、ワーキングツリーは何の変更もない状態に戻るため、問題なくgit pull
などが実行可能です。
→ git stash
は未コミットの変更を退避させるので、既にコミットしてしまったものはgit reset --soft
で元に戻す。
一時退避した変更はgit stash pop
というコマンドで復帰させることが出来ます。上述のように元々コミット済だったものは改めてgit commit
すればOK。
因みにこの一時退避ですが、実は配列のような構造をしており、複数のstashを管理して好きなものを取り出すということも可能です。git stash pop
はその中での最新の一時退避を取り戻すコマンド。
変更を適用し直すという仕様上git stash pop
でもpullした内容と箇所が被っていると競合を起こすことがあるので注意。その場合マーカーが出るのではなく普通にエラーが出てしまうので、一旦該当箇所を編集、コミットしてから再度popしましょう。
##diff, log
最後にgit status
のような感じで、所々使用する便利なコマンドを紹介します。
####diff - 差分を表示する
git diff
とだけ書くと、現在の状態と最終コミットとの差分を表示する。何も編集していなければ何も表示されない。
よく使うのはgit diff HEAD^
で一個前のコミットを参照すること。
pullなど行った時にdiffを確認することで、どういう変更が行われたのか確認出来る。
####log - コミット履歴を確認する
それまでのコミットを一覧で表示する。diffとは違い各コミットのコミットメッセージが羅列される形になるのでどういう変更が行われたのかざっくりと確認することが可能。
またコミット番号も書いてあるので、git reset
やgit revert
(またはgit checkout
)したいコミットを探す時に便利。
またgit log --graph
のオプションをつけることで、コミットグラフをグラフィカルに表示することも可能。
差分や履歴の確認は重要ですが、ターミナル表示だと結構見辛いのでGitHubやその他のGUIツールで確認することの方が多いかもしれません。ただコマンド自体は覚えておくに越したことはないので使えるようにしておきましょう。
##まとめ
共同開発になると競合(コンフリクト)が起こりうる状況が生まれるので、一気に難しく感じられるかもしれません(実際自分も結構混乱してました)。
- コミットグラフの仕様を理解する
- pushとpullを適切に繰り返す
- stashを活用する
途中に書いたものも含みますが、競合に対する苦手意識は上記のようなことを意識すると良い感じに解消されると思います。
特にstashコマンドは意外なほどに使う機会が多いので、push-pullのサイクルと合わせて活用して欲しいと思います!
次はいよいよブランチを用いて、業務におけるチーム開発について考えていきます。
前: プログラマー1年目の自分が辿り着いたGitまとめ その2
次: プログラマー1年目の自分が辿り着いたGitまとめ その4