Git のワークフロー(ブランチモデル)とその手順

Git クラウド

Git のブランチ機能を利用したワークフロー(ブランチモデル)について、実際の運用手順を説明いたします。

参考: Git を利用した開発環境・テスト環境・本番環境の構成


ブランチとは
ブランチ【Branch】とは、修正を行うソースコードのファイルセットのスナップショットです。
自身のクローン環境内にいくつでも作ることができ、それぞれのブランチでそれぞれ別の修正を施すことができます。
ブランチを切り替えることで、ワーキングディレクトリと呼ばれる作業領域に当該ファイルセットが復元されます。
Git ではこのブランチという機能を活用することでソース管理・バージョン管理を実現しています。

ブランチの切り替え


ブランチの運用モデル
ブランチの運用モデルについては、Vincent Driessen 氏が 2010 年に公開した有名なワークフロー図がありますね。

参考: A successful Git branching model

おおむねどのようなプロジェクトにおいてもこのモデルで運用していけば問題ないかと思いますが、ここでは最低限の運用ルールで実践可能なモデルについて説明したいと思います。

Vincent Driessen 氏のモデルもそうですが、ここから先の説明はあくまで”運用ルールの提案”であって、「こうでなければならない」とか「このようにしかできない」わけではありません。
Git は非常に柔軟であらゆる運用形態に対応できる能力を持っていますが、そのあまりの柔軟性の高さゆえに「どのように使えば良いのか」がわかりづらい要因になっているかと思います。

実際の運用で利用できる最もミニマムなモデルとしては、以下の3種類のブランチがあれば良いでしょう。

master ブランチ
git initfirst commitで作成される初期ブランチですが、運用時には本番リリース用のファイルセットとして利用されます。
最終的なテストが終了した必ず正常に動作するソースコードのみが本ブランチにpushされるべきであり、pushされたタイミングで本ファイルセットがプロダクション環境(本番環境)に適用されます(適用するようフックをかけます)。
中央リポジトリにてリモートブランチとして公開され、cloneしてきたローカル環境ではこれを追跡する同名(名前を変えることもできます)のローカルブランチ追跡ブランチ)が作成されます。

develop ブランチ
リリース前の検証テスト用のファイルセットです。
複数人のコード修正者から共有されるリモートブランチであり、修正のバッティング(コンフリクト)を解消したり、各々の修正をマージした結果、正しく動作するかどうか検証するために利用されます。
本ブランチにpushされたタイミングで当該ファイルセットがステージング環境(結合・システムテスト環境)に適用されます(適用するようフックをかけます)。
cloneしてきたローカル環境ではこれを追跡する同名(名前を変えることもできます)のローカルブランチ追跡ブランチ)を作成してこれを操作します。

feature ブランチ
コードの修正を行うローカルの作業ブランチです。
コード修正者の開発環境やテスト環境で利用されます。
リモートブランチではないため、コード修正者個人にしか見えません。
ブランチ名は任意ですが、慣例的には、feature/{issue}feature/{component}のように feature/ の後に解決しようとする不具合やコンポーネント名などをつけます。

リモートブランチとローカルブランチ

よほどのことがない限り、追跡ブランチ名をわざわざリモートブランチ名と違う名称にするようなことは、ややこしくなるだけですのでやめましょう。
リモートリポジトリのデフォルト名originも変更が可能ですが、こちらも複数のリモート参照をしていない限りはわざわざ変更しないようにしましょう。


develop リモートブランチの作成
masterブランチは Git 環境構築時に自動的に作成されますが、developブランチは自動的には作成されません。
リモートブランチはcloneしてきた自身のローカル環境から作成が可能です。
まずはローカル環境のブランチの状況を確認します。

>git branch -v # ローカルブランチの一覧を表示
* master  bb58af0 First Commit

>git branch -vv # 追跡先の情報も含めたローカルブランチの一覧を表示
* master bb58af0 [origin/master] First Commit

masterブランチが存在し、origin/masterというリモートブランチを追跡しています。
originとはリモートリポジトリ名で、リモートブランチは [remote/branch] という名前で表されます。
originはデフォルトで名づけられる名前ですが、git remote add等のコマンドで異なる名前での追跡が可能です。

次にリモートブランチのorigin/developを作成します。

>git push origin master:develop # git push origin [local:remote]
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To xx.xx.xx.xx:/path/to/remote.git
 * [new branch]      master -> develop

>git checkout develop
Branch 'develop' set up to track remote branch 'develop' from 'origin'.
Switched to a new branch 'develop'

1行目
ローカル環境のmasterブランチを元にして、リモートのdevelopブランチ(origin/develop)を作成しました。
この段階ではまだローカル環境のdevelopブランチ(追跡ブランチ)が作成されていません。

2行目
リモートブランチと同名の追跡ブランチを作成したい場合には、checkoutでブランチを切り替えるだけで作成まで行ってくれます。
すでに他の誰かがリモートブランチを作成済みであれば、このcheckoutだけで追跡ブランチの作成が完了します。

git branch developでブランチを作成してしまうと、developブランチが単なるローカルブランチとなってしまい、追跡が行われません。

ローカル環境がどのようになっているか確認してみます。

>git branch -vv # 追跡先の情報も含めたローカルブランチの一覧を表示一覧を表示
* develop bb58af0 [origin/develop] First Commit
  master  bb58af0 [origin/master] First Commit

ローカルブランチにdevelopが追加され、origin/developを追跡していることがわかります。
ここで、中央リポジトリのリモートブランチの状況を確認してみましょう。

>git fetch origin # リモート参照の更新
>git branch -r # リモートブランチの一覧を表示
  origin/HEAD -> origin/master
  origin/develop
  origin/hotfix # ★
  origin/master

まずfetchで中央リポジトリの最新の内容を取得してきます。
(ローカル環境で参照するリモートリポジトリの内容は、リモートサーバと接続しない限り更新されません)
-rオプションでリモートブランチの内容を表示ます。
他の誰かが新たにorigin/hotfix(★)というリモートブランチを作成したようですね。
もしorigin/hotfixブランチを追跡したければ、git checkout hotfixで完了です。
ちなみにorigin/HEADはカレントのリモートブランチで、ブランチ名を省略した場合などにこれが指定されます。

参考までに、リモートリポジトリの詳細を知るためのコマンドを紹介します。

>git ls-remote # リモートブランチ一覧
From user@xx.xx.xx.xx:/path/to/remote.git
bb58af04e8501a2e71962478a217d51d19f16d0c        HEAD
bb58af04e8501a2e71962478a217d51d19f16d0c        refs/heads/develop
bb58af04e8501a2e71962478a217d51d19f16d0c        refs/heads/hotfix
bb58af04e8501a2e71962478a217d51d19f16d0c        refs/heads/master

>git remote show origin # リモートブランチの詳細および追跡ブランチ
* remote origin
  Fetch URL: user@xx.xx.xx.xx:/path/to/remote.git
  Push  URL: user@xx.xx.xx.xx:/path/to/remote.git
  HEAD branch: master
  Remote branches:
    develop  tracked
    hotfix tracked
    master   tracked
  Local branches configured for 'git pull':
    develop merges with remote develop
    master  merges with remote master
  Local refs configured for 'git push':
    develop pushes to develop (up to date)
    master  pushes to master  (up to date)



ソースコードの修正からマスターアップまでの流れ
基本的には以下のような流れで進みます。

修正内容のブランチ間の流れ

① ローカルの develop ブランチを元に作業ブランチを作成
まずは作業ブランチを作成します。
仮に作業ブランチの名前をissue01とすると、以下のようなコマンドになります。

>git checkout -b issue01 develop # git checkout -b [作成ブランチ名] [派生元ブランチ名]
Switched to a new branch 'issue01'

checkout-bオプションはブランチの新規作成およびそのブランチへの切り替えを同時に行います。
派生元ブランチ名は省略可能ですが、必ず指定するように心がけましょう
思わぬブランチが派生元となり、トラブルを引き起こす原因となります。
派生元ブランチは最新の状態(手順は③の工程と同じです)にしたローカルのdevelopブランチが良いでしょう。
直接リモートのorigin/developブランチから派生させても良いですが、リモートの情報はfetchしないと最新にならないことに注意をしてください。
なお、作業ブランチをマージせず複数作成する場合は、直前の作業ブランチを派生元にします。

修正を commit する
ソースコードを修正し、修正内容を作業ブランチ(issue01)に反映・保存します。
もちろん単体テストはお済みですよね?

>vim sourcecode.php # ソースコードを修正
>git add -A # 変更内容をインデックスに登録
>git commit -m "issue01 commit" # インデックスの内容をブランチへ保存
[issue01 9741247] issue01 commit
 1 file changed, 1 insertion(+)
 ・・・

git add-Aオプションは変更があったすべてのファイルをインデックスに登録します。
-Aオプションがない場合には、新規に追加したファイルや削除したファイルが登録されません
git add .でも-Aと同じ働きをしますが、こちらはカレントディレクトリ配下に対してのみということで、どこのディレクトリで作業しているか注意が必要なので、-Aオプションの方が無難ですね。
git commit-mオプションでコミットに対するメッセージを入力できます。

ローカルの develop ブランチの内容を最新にする
origin/developからリモートの内容をpullし、ローカルのdevelopブランチを最新の状態にします。

>git checkout develop # developブランチに切り替え
Switched to branch 'develop'
Your branch is up to date with 'origin/develop'.

>git pull origin develop
From xx.xx.xx.xx:/path/to/remote
 * branch            develop    -> FETCH_HEAD
Updating 7207908..33a6cce
Fast-forward
 test.php | 1 +
 1 file changed, 1 insertion(+)
 ・・・

※マージの際にコンフリクトが起きる可能性があります。

Git 2.27.0 からgit pullをすると以下のようなメッセージが出力されるようになりました。

warning: Pulling without specifying how to reconcile divergent branches is
discouraged. You can squelch this message by running one of the following
commands sometime before your next pull:

  git config pull.rebase false  # merge (the default strategy)
  git config pull.rebase true   # rebase
  git config pull.ff only       # fast-forward only

You can replace "git config" with "git config --global" to set a default
preference for all repositories. You can also pass --rebase, --no-rebase,
or --ff-only on the command line to override the configured default per
invocation.

git pullの振る舞いに関して警告を出してくれているそうです。
--rebaseオプションについて知識のある方はご自身にとって適切な方を指定してください。
本サイトでは--rebaseは推奨しないので、デフォルトの振る舞いである false を設定します。
設定を行うとこのメッセージは出なくなります。

>git config pull.rebase false
 

④ 作業ブランチの内容をローカルの develop ブランチにマージする
issue01ブランチの修正をローカルのdevelopブランチにマージします。

>git merge --no-ff issue01 # issue01の変更内容をdevelopにマージ

# メッセージを記入するvimが立ち上がるので記入して保存

git merge--no-ffオプションは、ファストフォワード【Fast Forward】させないためのオプションです。
ファストフォワードとは、マージ元のコミット履歴(ブランチツリー)をマージ先のコミット履歴と(可能であれば)融合してしまうというものです。
ファストフォワードが起きるパターンとしては、例えば、developブランチの履歴が A←B←C で、featureブランチがCから派生して D←E←F となっている場合、

develop   A ← B ← C
                  ↑
feature           D ← E ← F

         ↓ git merge feature (into develop) ↓

develop,feature   A ← B ← C ← D ← E ← F

featureブランチの最終コミットFdevelopブランチにとっても最終形であるので、Git は自身の処理をシンプルにするため双方の履歴をつなぎ合わせ、HEAD ポインタをコミットF まで持っていきます。
--no-ffオプションを利用すると、このデフォルトの動きを抑止します。

develop   A ← B ← C
                  ↑
feature           D ← E ← F

         ↓ git merge --no-ff feature (into develop) ↓

develop   A ← B ← C ←---- G
                  ↑       ↓
feature           D ← E ← F

featureブランチの履歴がしっかりと残り、git mergeのためのコミットG が作られました。
このコミットGマージコミットといいます。
マージコミットがあることで、マージの取り消しやfeatureブランチでの一連の編集内容を見ることが容易になるというメリットがあります。
本サイトでは、マージの際の--no-ffオプションを推奨します。

※マージの際にコンフリクトが起きる可能性があります。

⑤ develop ブランチの内容をリモートの origin/develop ブランチへ push する
マージした内容を、origin/developpushします。
この作業により自身の修正を他者へ公開することになりますので、Slack などに通知することで他の作業者に周知し、更新を各々のローカル環境にマージしてもらうようにしておくと良いでしょう。
フックを利用していれば、この作業にて修正内容がステージング環境に適用されます。

>git push origin develop
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 3 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 462 bytes | 462.00 KiB/s, done.
Total 4 (delta 2), reused 0 (delta 0), pack-reused 0
To xx.xx.xx.xx:/path/to/remote.git
   9e12c4e..6432796  develop -> develop

⑥ develop ブランチの内容をローカルの master ブランチへマージする
前提として、⑤でpushした修正内容はステージング環境で十分にテストされ、問題ないことを確認する必要があります。
問題ないようでしたら、developブランチの内容をローカルのmasterブランチへマージします。
ここでも、--no-ffオプションをつけましょう。

>git checkout master # developブランチに切り替え
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

>git merge --no-ff develop

# メッセージを記入するvimが立ち上がるので記入して保存

master ブランチの内容をリモートの origin/master ブランチへ push する
いよいよマスターアップです。
フックを利用していれば、この作業にて修正内容が本番環境に適用されます。
非常に重要な作業ですので、基本的にはマスターアップ用のクローン環境を1つ用意し、専任者が当該環境にて作業をするように運用ルールを規定すると良いでしょう。

>git push origin master
Counting objects: 1, done.
Writing objects: 100% (1/1), 251 bytes | 251.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To xx.xx.xx.xx:/path/to/remote.git
   bb58af0..95c2f91  master -> master


ローカルの develop ブランチを起点とする
masterブランチとはすなわち本番環境ですので、常に正常動作する状態に保たれていなければなりません。
したがってその1つ手前のdevelopブランチですべての問題を事前に解決しておく必要があります。
developブランチはリモートブランチであり複数人の作業者に共有されているので、他者の修正をここで初めてマージするわけですが、このマージ作業は様々な問題を引き起こします。
例えば同じファイルを修正したことによって発生するコンフリクト状態や、他者の修正と自身の修正が組み合わさることによって生じる不具合などが起こる場合があります。

これらの問題への対処方法としては、なるべく早めに他者の修正を自身の環境に取り込んでおくことが重要になってきます。
マージ対象の変化が大きければ大きいほどコンフリクトのリスクは高まりますし、自身の修正への影響も大きくなります。
互いの修正部分が複雑に入り組んだ状態になってしまうと、最悪自分の修正コードはもう一度作り直しになってしまう可能性もあることでしょう。
masterから作業ブランチを作るよりも、他者の修正がすでに入ったdevelopから作業ブランチを作ることで問題の早期発見につながり、修正リスクを軽減できると言えるかと思います。
Vincent Driessen 氏のワークフロー図でも作業ブランチはまずdevelopから派生しています。

以上のことからローカルのdevelopブランチは常に最新の状態に保っておくことも Git の運用にあたっては重要となります。
リモートのdevelopブランチが更新されたら Slack などに通知することで他の作業者に周知し、通知を受け取ったらすばやく更新を自身のローカル環境にマージしてください。

下図のようにブランチ間の関係を図で表してみると、ローカルのdevelopブランチが起点となっている様子がわかりますね。

起点となるdevelopブランチ


ブランチツリーの確認
最後にブランチツリーを確認してみましょう。
git logコマンドの--graphオプションでツリーを見ることができます。
--formatでツリーの形が見やすいよう出力内容を調整しています。

# [修正者名] [コミットID] [ブランチ名] [コミットメッセージ1行目]
>git log --graph --all --format="%an %h %d %s"
* John Doe 062e67a  (HEAD -> issue04) issue04 commit
*   John Doe 04aeba8  (develop) Merge branch 'issue03' into develop
|\  
| * John Doe 522ba8d  (issue03) issue03
|/  
* John Doe 5adf32e  (issue02) issue02 commit
*   John Doe 0601cf9  Merge branch 'issue01' into develop
|\  
| * John Doe d3b712a  (issue01) issue01
|/  
* John Doe bb58af0  (origin/master, origin/develop, origin/HEAD, master) First Commit


これくらいのツリーであればなんとか見えなくもないですが、さらに複雑なツリーになるとgit logコマンドではさすがに視認しづらくなってきます。
このようなときは、GitKraken や SourceTree といった無償の Git クライアントツールを利用すると良いでしょう。
下図は SourceTree でログを見た場合の画面です。

参考: GitKraken
参考: SourceTree

ブランチツリー


参考: Git コンフリクトの解消

コメント