Gitの基礎知識と活用方法
はじめに
4日目、よしだです。
今まで何となくで使っていたGitを改めて一から勉強してみた。コマンドとかは調べればすぐに出てくるので詳しくは触れず、ここでは主に基本的な概念とGitの機能を活用した開発フローについて紹介する。
Gitとは
ソフトウェア系の人は少しは触れたことがあるとは思うが、中には全然知らない人もいると思うのでGitについて簡単に紹介しておく。
Gitはプログラムのソースコードなどの変更履歴を記録・追跡するための分散型バージョン管理システムである。
(Wikipediaより抜粋)
端的に言えばプログラムにいつ・誰が・どのように変更を加えたかを記録しておくためのシステムである。Git誕生以前にもバージョン管理システムというものは存在していたが、今はおそらくGitが最も使われているだろう。その理由としては分散型であるのが大きいのではないだろうか。分散型とは開発者ひとりひとりのローカル環境に全員の変更履歴のコピーが作成されている構造のことで、これによって各開発者がどんどん開発を進めていってあとで一つにまとめるというような開発フローが実現できるのが特徴。
リポジトリ
Git (というよりバージョン管理システム全般) を使う上でまず押さえておきたいのがこの「リポジトリ」の考え方である。リポジトリとは ファイルやディレクトリの状態を保存する場所という扱いになっている。Gitのリポジトリにはリモートリポジトリとローカルリポジトリの2種類が存在し、これが分散型の特徴。
リモートリポジトリ
GitHubなどの専用のサーバー上に存在するリポジトリで、複数人でリポジトリの情報を共有する目的で使われている。
ローカルリポジトリ
ひとりひとりのマシン上に存在するリポジトリで、個人でのリポジトリ管理を行う
リモートリポジトリとローカルリポジトリは基本的に切り離されており、必要に応じてそれぞれの状態を反映する。ローカルリポジトリからはリモートリポジトリの場所 (URL) は origin という名前で見えている。
普通は開発者が自分のローカルリポジトリ内で開発を進めていき、ある程度形になったらリモートリポジトリ内で一つにまとめるという流れ。
リポジトリの作成・設定
ローカルリポジトリではGitの情報は .git/ というディレクトリで管理されていて、 .git/config/ の中にユーザー名やらメールアドレスやらの個人情報やリモートリポジトリのURLなどの設定を保存する。
新しくローカルリポジトリを作ってそれに関連付けたリモートリポジトリを作ることもできるし、既に出来上がっているリモートリポジトリを元にローカルリポジトリを作りたければ クローン することでリモートリポジトリの情報などをそのまま複製して持ってくることができる。
table: 関連コマンド
git config git init git remote git clone
変更内容の更新
ワークツリー (作業しているディレクトリ) 内のファイルの変更内容をローカルリポジトリに反映することをコミットという。変更内容をコミットするためにはインデックスに登録する必要がある。
なぜコミットする際に毎回インデックスに登録するのか?ワークツリーから直接リポジトリにコミットしてもいいように思うが、例えば、複数のファイルを一気に編集したが、コミットの回数は分けたいときに一部のファイルだけインデックスに登録してコミットするみたいなことができる。
変更内容をコミットすると 40 桁の長々とした文字列 (ハッシュ値) が振り分けられ、いつ、誰がコミットしたかの情報とコミットする際につけたコメントを結び付けて保存される。そのハッシュ値を元に昔のコミットの変更箇所を見たり、そのコミットに戻ったりできるが、その時は頭の最低 4 桁の情報だけ使えばよい。
最新のコミットに関しては HEAD という名前でもアクセスすることが可能。
table: 関連コマンド
git commit git add git log git reset git revert
ブランチ
プロジェクトを枝分かれさせて同時進行で開発を行うための機能。バージョン管理といってもただ一直線に時系列順に管理していくのではなく、機能などの単位でそれぞれ管理することができる。実装する機能ごとにブランチを切り分けて開発を行い、形になったらそれぞれの変更をまとめて一つのプロジェクトを完成させる。デフォルトのブランチは master という名前になっている。ブランチを切り替えることをチェックアウトと呼び、master からブランチを切って開発を進めていくのが一般的。
table:関連コマンド
git branch git checkout
リポジトリの状態を確認する
status
リポジトリ内のファイルの状態を確認する。現在どのブランチにいるとか、どのファイルがコミットされる予定なのかなど。
log
コミット履歴を確認する。いつだれがどのコミットを行ったかの履歴が羅列される。視覚的にわかりやすいツリー構造で表示することも可能。
diff
コミット間やブランチ間などの差分を表示する。
タグ付け
Gitにはタグ付けという機能があり、特定のコミットにタグをつけることで参照を容易にすることができる。長々としたコミット番号の代わりにタグを使うことでそのコミットへの参照が容易になる。バージョン管理をする際に、リリースしたバージョン番号などを重要なコミットにタグ付けをしておくことで管理しやすくする。
table:関連コマンド
git tag
ローカルの変更内容をリモートに反映する
ローカルリポジトリの変更履歴をリモートリポジトリに反映するときにはプッシュする。
$ git push origin xxx_func
のようにURLの名前とブランチの名前を指定することで、目的のリモートリポジトリにプッシュすることができる。
table:関連コマンド
git push
リモートの状態をローカルに反映
リモートリポジトリの変更内容をローカルリポジトリに持ってくることをフェッチという。この段階では origin の中が更新されるだけで、ローカルリポジトリ内の変更内容が上書きされるわけではない。変更内容をワークツリーに反映させるには マージ する必要があるが、プル することでフェッチとマージを同時に行うこともできる。プルの方がひとつのコマンドで済むので楽だが、リモートリポジトリをいじっている人への信頼がないならやめておこう。フェッチして diff などで確認してからマージしたほうが安心。
一応説明しておくが、ローカルからみた origin は常に最新が更新されているわけではない。そのためにフェッチというものが存在しているし、それが分散型の特徴なのだ。
table:関連コマンド
git fetch git merge git pull
ブランチを結合する
マージ
他のブランチの変更内容を反映すること。マージ先が変更を加えられていたならコミットがまとめられ、マージコミットが作成される。一方でマージ先 (ここでは master) が変更されていない場合は master が指す先のコミット番号が差し替えられるだけの Fast-Forward マージが行われる。他のブランチのとあるコミットだけ持ってくる Cherry-pick という機能もある。
リベース
ブランチの分岐点を移動させること。例えば、master から切り出した開発用のブランチに master の変更内容を反映したいときにマージの代わりにリベースを行うと、現在の master からブランチを切ったような履歴に書き換わる。マージしたときと異なる点はリベース前のコミット履歴が一直線になるところである。後述のコンフリクトを解消する際に使われることがあり、あらかじめリベースを行った状態で master にマージをすると開発用ブランチの変更が反映される。
リベースをする注意点として、プッシュした後にリベースしないこと を心がけておかないと、ローカルブランチとリモートブランチで分岐点が異なってしまい混乱を招く可能性が生じる。
マージかリベースどっちがいいのか。
基本的にはマージをお勧めする。リベースだとコミット履歴が一本になってパッと見はきれいだが、誰がいつ何を変更したかがわかりにくくなり、分散型のポリシーとはちょっとずれてしまう。周りに影響を与えないちょっとした変更で、変にコミットを分岐させて見にくくする必要はないなどのときだけこっそりリベースしよう。
table:関連コマンド
git merge git rebase
https://gyazo.com/7078473c3d51f240f6fe02b4d36d8326
Gitを活用した開発
ここまで紹介した機能が使えるようになれば十分開発に活用することができるだろう。ここからはGitのバージョン管理機能を利用した基本的な開発の流れと、ブランチを使った開発フローについて説明する。
基本的な開発の流れ
1. リポジトリを作る
2. ワークツリーで作業する
3. インデックスに登録する
4. コミットする
5. プッシュする
のようになっていて、2~4 を繰り返して一段落着いたら 5 に進んで 2 に戻るような感じ。必要に応じてブランチを切ったり、フェッチして現在のリモートリポジトリの状態を確認しながら開発を進めていく。ブランチの実装が完了したら随時マージしていく。
共同開発における注意点
同一のリモートブランチに複数人で別々に変更を加える際に、同じファイルの同じ箇所を変更することはしばしば起こり得る。こういう時はコンフリクトが生じてしまい、プッシュやマージができなくなる。コンフリクトが起きたときは該当ファイルのコンフリクトしている箇所が
code: xxx.cpp
<<<<<<<HEAD
"自分が変更を加えたコード"
=======
"マージするブランチ内のコード"
のようになっているので、status でどのファイルが該当するか確認し、 "HEAD" で検索してコンフリクトを解消し、もう一度マージを試みる。コンフリクトが解消されているなら正しくマージが行われる。
コンフリクトが起こりにくいようにするためには定期的にリモートリポジトリの変更を反映することが重要である。
ブランチ戦略を活用した開発フロー
ブランチを活用した開発フローの代表として git-flow とGitHub Flowというものがある。
git-flow
maste ブランチは常にリリースできる状態に保たれている
master, develop にコミット、プッシュすることはない
機能追加するときは master から develop ブランチを切り、develop からさらに機能ごとに feature/xxx_func のようなブランチを切る。feature ブランチは開発が完了すると develop ブランチにマージされ、全ての機能追加が終わると develop ブランチを release ブランチにマージする
リリースするときは release から master にマージしてタグ付けする
リリース後のバグは hotfixes ブランチを切って対応し master にマージする
このフローで最も気をつけるべきことは、master の扱いである。master が常にリリース可能な状態に保つことを満たすために他のルールがあると言ってもいい。コードに手を付けるのは必ず master 以外のブランチ内で、コードの品質が保証されて初めて master にマージすることができる。
develop ブランチや release ブランチだが、これに対応するものは後述の GitHub Flow には存在しない。その理由として、このgit-flow で重要視されているのが リリース だということに起因する。ソフトウェアをリリースするときには完成されていなければいけないので、全ての機能が完全に実装が終わってからマージするための develop ブランチや、リリースに向けた作業を行うための release ブランチが必要となってくる。そういう意味で develop ブランチや release ブランチもある程度の品質は問われていて、develop ブランチであれば、マージされている個々の機能が動作すること、release ブランチでは全ての機能の動作は完成されていることが求められる。
https://gyazo.com/7d87d0ec424d770dabdab0454b358084
GitHub Flow
基本思想は git-flow と同じだが、ブランチの規則が少し異なる。git-flow に比べてシンプルなブランチ構成になっているのが特徴で、この構成の根底には デプロイ を重要視している考え方が存在している。デプロイとは、簡単に言えばリリースだけでなくテスト等も含めてプログラムが動作可能な状態になっていることである。 GitHub Flowの特徴を以下に示す。
master は常にデプロイ可能な状態になっていること
master にコミット、プッシュすることはない
機能ごとに feature ブランチを master から直接切る
軽微なバグ修正は bugfix ブランチを切る
作業が完了したブランチは master にマージする
master の役割がリリースではなくデプロイであることから、master に求められることが「確実に動作すること」に絞られている。全機能が実装されている必要や、リリースの準備が整っている必要はない。そういう観点から develop ブランチや release ブランチは存在していない。一つ一つの機能ごとに master にマージされるので、master の品質は git-flow よりも低いかもしれないが、アクティブなブランチの数が少ないため管理はしやすい。
https://gyazo.com/1c23ee6965b894973673d21d1fadf2f5
結局のところ、どういった開発フローで進めていくかはそれぞれの成果物の扱いや組織のカラーが影響してくるもので、一概にはどちらがより優れているということではないんだと思う。
ただし、どちらのフローにおいても master の品質が保証されていることと master にコミット、プッシュをしないことは共通している。Gitで開発する上で master は神聖なブランチであることを意識しなければならないということだろう。初心者向けのサイトとかを見ると平気で origin master にプッシュとかするやり方が載っているので、ちゃんと意味を理解せずに使ってるとわけわからんことになってしまう。(実際、筆者も最初個人で使っているときは普通に master にコミット、プッシュしていた)
また、切り出したブランチは必ず最終的に master にマージすることを忘れてはいけない。ちょっとした機能の差異があるプログラム開発を行う場合に、開発ブランチからさらにブランチを切って広がっていったりすると、せっかく master を中心に開発している意味がなくなってしまう。リリースのバージョンを区別したければタグ付けをするか、あまりにも内容が異なるバージョンがあるなら master を完全に分けてしまった方がいい。
プルリクエストやCIツールなどの機能やツールを使うことで master の品質を向上することもできる。
プルリクエストとは
GitHubなどの機能で、レビュアーに対して「プルしてください」というリクエストを出すこと。どこを変更したかとかどういう意図のもとで変更を加えたかとかを記入してプルリクエストを出す。これを受けたレビュアーがローカルで master にプルしてビルドやテスト等の動作確認をしたり、コードレビューをする。問題なく承認されて初めてリモートの master にマージされることになる。
マージのためのレビューだけでなく、開発途中でコメントがほしいときなどにも使える
CIツール
Jenkinsなどに代表されるCI (Continuous Integration) ツールを使って master マージ時に自動で結合テストや静的解析などを行うことで、常に master をデプロイ可能な状態に保つことができる
さいごに
Gitのコマンドとか使い方はググればある程度出てくるので、あまりちゃんと理解していなくても使えるのは使えるが、基礎から学びなおすことでどうすればちゃんと使いこなせるかとかが見えてくるかも。
大規模な開発での使い方をメインで説明したが、個人でソフトウェアを管理している場合でもただのバージョン管理ツールとして使うだけでなく色々工夫しながら使えればいいね。
まだまだ勉強途中なので有用な情報あれば共有してもらえるとありがたい。