概要
プログラマー1年目の自分が辿り着いたGitまとめ その1
プログラマー1年目の自分が辿り着いたGitまとめ その2
プログラマー1年目の自分が辿り着いたGitまとめ その3
の続きです。
最後は所謂チーム開発の環境である『複数人での開発』を想定していきます。
これまでとは違ってブランチという機能を使うことに注目します。
一旦振り返り
その1~3で紹介したコマンドで出来ることをまとめてみると
- リモートリポジトリでソースコードを共有
- 複数人で異なる箇所を開発
- 各々が担当箇所についてコミットを重ねていき、必要に応じて別メンバーの変更を取り込む
- 競合が発生した場合は、それぞれの意見に合わせて解決し、プッシュ
これだけでもかなり十分で、簡単な開発なら問題ないと思います。
ただ業務での開発となると、より効率を高めるための工夫が必要になってきます。
逆に現状での問題点、及び足りないものを挙げてみると……
- 不具合が起きた時にデバッグが行いにくい(変更を巻き戻す必要がある)
- バグのあるコードを共有してしまう可能性がある
- 開発箇所を分けていることがわかりにくい
といった感じでしょうか。
これまでも何度か同じ例えをしてますが、バージョン管理をRPGのセーブ機能だとすると、今はセーブと巻き戻しは出来るけれど、肝心のセーブファイルが一つしか作れないという状況に思えます。
Gitにはブランチという機能があり、これを使うことで上記のような問題を解決し開発を円滑に進めることが出来ます。
Gitで最も大切な機能とも呼べる一方で、わかりにくい箇所の筆頭でもあるかと思われるので、ここが理解できると大分見通しが良くなります!(経験談)
checkout, branch
ブランチという機能に触れる前に、デバッグを行いやすくするcheckoutコマンドを紹介します。
checkout - 特定のコミットに移動する
checkoutコマンドはステージ前の変更を元に戻すのにも使われるらしいんですが
実際は専らこっちの用途に使われると思います(前者はIDEとかの機能で大体どうにでもなるので)
git checkout コミット番号
とすることで、そのコミット時点での内容にソースコードを切り替えてくれます。
前々回にはgit reset --soft
などで重ねてきたコミットを一旦消すことで変更を巻き戻すという方法を紹介しましたが、このコマンドだと単純に『そのコミットに切り替える』という動作なので、コミットを消したり追加したりというような副作用はありません。
なので不具合が出た際などに、気になる場所に戻ってビルドし直し、その不具合が起きるかどうか検証……みたいなことが簡単に実行できるようになります。
detached HEADについて
このコマンドでコミットを移動すると『detached HEAD』状態になります。
参考: https://yu8mada.com/2018/05/31/detached-head-state-and-its-caution-in-git/
(コミットA)――(コミットB)――(コミットC)――(コミットD) ← master
↓ コミットBにcheckout
(コミットA)――(コミットB) ← detached HEAD
簡単に言うと過去のコミットに戻った上で、検証のために新たなコミットを積み重ねていくということが可能な状態です。
新たに行ったコミット達はmasterに戻った時に巻き戻されるため、このコマンドは不具合などが発生した場合に原因を調査し、修正を行う為の環境として役立てることが出来ます。
checkoutしたコミット部分から、自由に変更をコミットしていける
(コミットA)――(コミットB)――(コミットE)――(コミットF)――(コミットG) ← detached HEAD
↓ 再びcheckout master
detached HEAD状態のコミットは破棄される
(コミットA)――(コミットB)――(コミットC)――(コミットD) ← masterはこのまま
こうした一時的なコミットはデバッグに便利ですが、一時的であるというのがデメリットになってしまう場合もあります。
例えば上記の例で、コミットBにcheckoutし、幾つか修正のコミットを行ってみたところバグが無事解決したとします。
しかしコミットC及びDの変更を再び加えるためにmasterにcheckoutすると、バグを修正したコミットは一時的なものだったとして消えてしまう訳です。
上記のようにcheckoutした箇所から『派生させた』コミット群を、一時的なものではなくきちんと記録して残しておきたい……という場合の為に、ブランチという仕組みが存在しています。
branch - 現在のコミットに別名をつける(ブランチを作成する)
まずブランチとは何かというと、これは最新コミットの名前を表します。
これがどういうことかというと、Gitのリポジトリにデフォルトで存在しているmasterブランチを考えると分かりやすいです。
(コミットA)――(コミットB)――(コミットC)――(コミットD) ← master(コミットDを指している)
↑ その前のmaster
リポジトリの最初のコミットが、まずmasterという名前を持ちます。
そこからコミットを重ねる毎にmasterは最新コミットに移り変わっていきます。上記の例ではコミットDがmasterですが、その前はコミットCがmasterだったし、その前はコミットBがmasterだった、というイメージです。
ここで先程のdetached HEADの例に戻り、コミットBにcheckoutした場合を考えます。
(コミットA)――(コミットB)――(コミットC)――(コミットD) ← master
↓ checkout
(コミットA)――(コミットB)――(コミットE)――(コミットF)――(コミットG) ← detached HEAD
この状態でgit branch (ブランチ名)
を実行することで、一時的なコミットに名前をつけ記録として残しておくことが出来ます。例えば今回の場合バグを修正したのでbugfixと名前をつけるとすれば、
git branch bugfix を実行
(コミットA)――(コミットB)――(コミットC)――(コミットD) ← master
|
――(コミットE)――(コミットF)――(コミットG) ← bugfix
といった感じに最新コミットGに名前が付きます。
Gに続くコミットを行えば、今度はそのコミットがbugfixになります。そして一度ブランチを作れば、git checkout master
としてもコミットは破棄されず、再びgit checkout bugfix
とすれば戻ってこれる、という仕組みです。
checkout -bについて
既に修正内容がコミットされた状態からブランチを作る、という例を説明しましたが、どちらかというと逆のほうが多いです。
例えばバグ修正ではなく、新しい機能追加を行うと言った場合に、これまでの状態を一旦セーブしておいた上で別の機能開発に取り掛かるといったシチュエーションです。
先程ブランチは最新コミットの名前だと説明しましたが、branchコマンドはその最新コミットのエイリアスを作成するコマンドです。
(コミットA)――(コミットB)――(コミットC) ← master
この状態で、開発用のブランチとしてgit branch develop
と実行すると、
(コミットA)――(コミットB)――(コミットC) ← masterであり、develop
という状態が生まれます。コミットCはmasterという名前を持っていましたが、branchコマンドによってdevelopという別名をつけられた訳です。
ここでgit checkout develop
を実行し、ブランチを切り替えてコミットすることでdevelopが最新コミットに移動していきます。この時当然ながらmasterは変わらないため、これまでの状態を保ったまま新しい開発を進めていく、といったことが可能になります。
(コミットA)――(コミットB)――(コミットC) ← masterはこのまま
|
――(コミットD)――(コミットE) ← develop
branchコマンドとcheckoutコマンドをそれぞれ実行するのは面倒なので、こういう場合は-bオプションを使います。
git checkout -b develop
とすることで、上記のブランチ作成&移動という部分を一度に行うことが出来るので、よく使うオプションです。
merge
bugfixブランチの例に戻ると、現状としては
(コミットA)――(コミットB)――(コミットC)――(コミットD) ← master
|
――(コミットE)――(コミットF)――(コミットG) ← bugfix
というコミットグラフになっており、
- masterブランチ → 機能的に最新状態だけど、バグが有る
- bugfixブランチ → バグは治ったけど機能的に最新ではない
という状況です。
これを一つにまとめて『バグも直った最新版』を作るのがmergeコマンドです。
merge - ブランチを統合する
git checkout master
でmasterブランチに戻ります。
(この時git status
で自分が今どのブランチにいるか分かるので確認する癖をつけておくと良いと思います)
masterブランチにいる状態で、
git merge bugfix
を実行することで、bugfixでの変更を取り込みます。
masterブランチで git merge bugfix を実行
(コミットA)――(コミットB)――(コミットC)――(コミットD)――(マージコミット) ← masterはここに移動
| |
――(コミットE)――(コミットF)――(コミットG)――
↑ bugfixはここのまま
mergeを実行するとマージコミットという新たなコミットが作成されます。
このコミットはマージをしたという目印のようなものであり、内容としては二つのブランチの変更を全て取り込んだコミットです。
変更を取り込んだmasterブランチは最新コミットであるマージコミットに移動します。
一方、bugfixブランチの位置は変わりません。
bugfixブランチでmarge masterとやってしまうとこの関係が逆になってしまうので注意。
mergeコマンドは指定したブランチ『を』現在のブランチに取り込むコマンドです。
競合とfast-forwarded
上記の例ではコミットC,DとコミットE,F,Gは共にコミットBから連ねっている『並列なコミット』です。
また元々masterにあったバグを修正しているためコードに食い違いがあり、この為mergeの際に競合が発生します。
mergeの際に競合が発生するのはよくあることなので、前回同様競合マーカーを探して該当する箇所の競合を解決しましょう。今回の場合はbugfix側の変更を残すのが良さそうです。
修正が終わったらそれらを改めてコミットすることでマージが完了します。
ここで、masterブランチからdevelopブランチを切った例に戻ってみます。
(コミットA)――(コミットB)――(コミットC) ← master
|
――(コミットD)――(コミットE) ← develop
この場合にmasterブランチでgit merge develop
を行うと、先程の例とは異なり並列になっているコミットは存在しないため競合は発生しそうにありません。
また、わざわざコミットD,Eの変更を統合したマージコミットを作る必要もなさそうです。
なのでこの場合、
masterブランチで git merge develop を実行
(コミットA)――(コミットB)――(コミットC)
|
――(コミットD)――(コミットE) ← developもmasterもここになる
という風にmasterがdevelopの位置に直接移動します。マージコミットは作成されません。これをfast-forwardedなマージといい、上記のように並列なコミットが存在しないパターンで発生します。
因みにマージコミットを残したいという人のために、fast-forwardedになってしまう場合でもそれをキャンセル出来るオプションが存在します。
厳密に履歴を管理する場合はfast-forwardedを使わないことが推奨されますが、そこまで拘る必要はないかも知れません。ただマージコミットが作成されたりされなかったりで混乱しない為にも、知っておくべき仕様だと思います。
fetch
自分で作ったブランチ(ローカルブランチといいます)をローカルリポジトリ内でマージするという例を紹介しましたが、ブランチは勿論リモートにプッシュすることも可能です。
というより業務の場合普通はリモートにプッシュしたものをGitHub上でレビューしてもらい、承認を得た上でマージする、というのが基本的な流れになると思います(所謂プルリクエストというやつです)
ローカルブランチをプッシュする場合は、リモートにまだそのブランチが存在しないためgit push
だけではエラーが出ます。なのでgit push origin develop
のようにリモート及びブランチ名を指定してやることで、リモートへのブランチの作成とプッシュを同時に行ってくれます。
ではリモートに新しくプッシュされたブランチを取り込むとき、pullの場合は? というと、ここで大事なのがfetchコマンドと追跡ブランチです。
追跡ブランチ
追跡ブランチとは、ローカルリポジトリのブランチとは別に存在する『リモートリポジトリの状態を反映したブランチ』です。
上の方では割愛しましたがgit branch
を実行すると、現在のローカルブランチの一覧が見られます。ここに-aオプションを加えてgit branch -a
とすることで、ローカルに加えてリモートのブランチも参照することが出来ます。
git branch
master
* develop
git branch -a
master
* develop
origin/master
origin/develop
追跡ブランチは『origin/(ブランチ名)』といった名前で存在し(厳密にはリモート名とブランチ名ですが、大抵の場合はoriginです)、リモートブランチの変更を保持しています。
勿論、これらのブランチにcheckoutすることも可能です。
fetch - リモートの状態を追跡ブランチに反映する
ではこれらの追跡ブランチが常にリモートの内容と一致しているかというとそういう訳ではなく、git fetch
を実行することで同期が行われます。
例えばリモートのdevelopブランチに何かしらの変更がプッシュされたとして、ローカルには未だ何も反映されません。
そこでgit fetch
を実行することで、ローカルのorigin/developブランチにリモートの変更が反映されるという訳です。
同様に、リモートに新たなブランチ(例えばbugfix)が作成されたという場合にもgit fetch
を行うことで対応する追跡ブランチを作成してくれます。
git branch -a
master
* develop
origin/master
origin/develop
git fetch
git branch -a
master
* develop
origin/master
origin/develop
origin/bugfix ← 新しく追加される
そしてこのブランチをローカルブランチとして作成する、という場合には
git checkout -b bugfix origin/bugfix
とすればOKです。checkout -bは第2引数を指定することでブランチの派生元を指定できます。
因みにgit fetch
した時点で実は『リモートにはbugfixブランチが存在する』ということがGitには分かるので、単純にgit checkout bugfix
としてもブランチの作成が自動的に行えてしまったりします。
ただ間違ってもgit pull origin bugfix
のように書かないようにしましょう。pullの紹介時にも書きましたが、ダイレクトにリモートのbugfixを現在のブランチに取り込んでしまうため履歴がめちゃくちゃになります。
pull = fetch + merge
上記の例でgit fetch
を行うと、developブランチに変更があった場合origin/developブランチがリモートの状態に更新される、と書きました。
ここで重要なのはローカルのdevelopブランチにはまだ何の変更も行われていない、ということです。
developブランチにも変更を取り込むという場合は、developブランチでgit merge origin/develop
を行います。
この一連の動作は、developブランチにおいてgit pull
を行った場合と一致しています。
なので実はgit pull
はfetch + mergeです。
内部でmergeを行っているためにpullの際に競合が発生する場合がありますし、fetchを行っているので、一度pullを行ったブランチは追跡ブランチが作成されているという訳です。
リモートリポジトリにプッシュされた内容を試したいけれど、変更は取り込みたくないという慎重なシチュエーションでは、git fetch
だけしておいて、追跡ブランチにcheckoutしてアプリをビルド、という風にすればローカルに変更を取り込まずにそのブランチを実行できます。
別の人がプッシュしたブランチの状態で実行やテストを行いたい、という場合にはfetchコマンドが有用です。その上で編集や新たにプッシュを行いたい、という場合にはローカルブランチにcheckoutする、というのが良いやり方だと思います。
まとめ
ブランチという大きな機能について一気に書いてしまったので、ちょっとまとまっていない部分もあるかもしれません。
個人的に大事だと思うのは、ブランチという名前からグラフのようなイメージがあるそれについて、実体を正しく把握することかと思います。
- ブランチとは、最新のコミットに付く名前でありコミットが重なる毎に移動していく
- ブランチを作るというのは、現在のコミットに別名をつけるということ
mergeやfetchよりも正直ここを理解することが、一番Gitを使いやすくするポイントだと感じました。
かなりざっくりとしたシリーズになりましたが、初学者の方の助けになれば幸いです!