git branchとmergeの使い方:枝を分けて合流する、GitHub Flowまで

公開 更新 書いた人 大林 諒

ブランチ(枝)とmergeは、Gitの説明で必ず出てくるのに、必要な場面が分かりにくい機能です。

この記事では、サンプル料理教室の料金表を枝で直して合流させるところまでを、実際に動かした表示つきで読みます。あわせて、GitHub Flow、Git Flow、GitLab Flowの違いも整理します。読み終わると、自分の会社で枝が要るのかどうかを判断でき、外注先の話にも付いていけるようになります。

この記事の内容
  1. 先に答えると
  2. ブランチとmainの話
  3. 一人ならmainだけでよい
  4. 枝を切って合流するまでを動かす
  5. fast-forwardと3-wayマージ
  6. git checkout と git switch の関係
  7. GitHub Flow・Git Flow・GitLab Flowの違い
  8. 枝の名前の付け方
  9. 終わった枝の消し方
  10. できないことと、その代わり
  11. よく詰まるところ
  12. 次に読む

先に答えると

ブランチは、本筋を止めずに変更を試すための別ルートです。合流させる操作がmergeで、終わった枝は消します。一人でホームページを直しているだけなら、枝を使わず main だけで問題ありません。

  • 枝が要るのは、人と一緒に作業するとき、本番に出す前に確かめたいとき、AIに大きな変更をさせるときの3つです。
  • 基本の流れは git switch -c 枝の名前、コミット、git switch main、git merge 枝の名前、git branch -d 枝の名前 です。
  • 古い説明の git checkout は、枝の切り替えなら git switch と同じ働きです。
  • 運用の型は3つ有名ですが、小さな会社はGitHub Flowで足ります。

ブランチとmainの話

mainは、リポジトリ(ファイルと履歴をまとめて入れておく箱)の本筋になる枝です。GitHubで新しく作るリポジトリの既定の枝の名前が main で、clone(手元への複製)をしたときも最初にこの枝が開きます。古いリポジトリでは master という名前のこともあります。

枝が役に立つのは、作業が割り込むときです。代表が夏の教室のページを作っている途中で、受付Aから「月謝の表示が古いので今日中に直してほしい」と連絡が来たとします。作りかけのページをmainで編集していると、月謝の修正だけを先に出すことができません。夏の教室の作業を枝に分けておけば、mainに戻って月謝だけ直せます。作りかけは枝に残ったままです。

一人ならmainだけでよい

代表が一人で料金表を直してGitHubへ送るだけなら、枝は要りません。main にコミットして送れば済みます。覚えることを増やす必要はありません。

枝が要るのは次のときです。

場面枝を切る理由
外注エンジニアBなど人と同じリポジトリを触るお互いの作りかけが混ざらない。プルリクエスト(Pull Request、PR)で見てもらえる
本番に出す前に確かめたいmainに入れる前に、枝の上で動きを見られる
AIに大きな変更をさせる気に入らなければ枝ごと捨てられる。mainは無傷で済む

3つ目は実際に運用していて効きます。AIに複数のファイルをまとめて書き換えさせるときは、先に枝を切り、変更を枝の上でコミットしておきます。気に入らなければ git switch main の1回で元の状態に戻れます。

mainから枝を切って、直してから合流する
mainから枝を切って、直してから合流する

枝を切って合流するまでを動かす

ここからの表示は、すべて実際に動かしたものです。GitHubの代わりに、手元のフォルダに練習用のリモート(共有の置き場となるリポジトリ)を作って試しました。そのため送り先がフォルダの形で出ます。本物のGitHubならURLになります。ハッシュ(9033672 のようなコミットの番号)は毎回違う値になります。

枝を作って移る

$ git switch -c fix-menu-price
Switched to a new branch 'fix-menu-price'

$ git branch
* fix-menu-price
  main

-c は作って移る指定です。git branch は枝の一覧で、* が今いる枝です。

枝の上でコミットする

menu.html の体験レッスンを3,000円から3,500円に変えて、コミットします。

$ git add menu.html

$ git commit -m "料金表を修正"
[fix-menu-price 9033672] 料金表を修正
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline --graph --all
* 9033672 (HEAD -> fix-menu-price) 料金表を修正
* 84b3ca8 (origin/main, main) ホームページの最初の版を登録

git log --oneline --graph --all が、枝の様子を見るコマンドです。--graph で線の図を付け、--all で全部の枝を出します。カッコの中が枝の札で、上が新しいコミットです。HEAD -> は今いる枝、origin/main はGitHub側の main の控えです。

mainへ戻る

ファイルの中身まで戻ることを確かめます。

$ grep 体験 menu.html
<p>体験レッスン 3,500円</p>

$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ grep 体験 menu.html
<p>体験レッスン 3,000円</p>

枝を切り替えると、フォルダの中のファイルがその枝の状態に変わります。修正が消えたように見えて驚くところですが、枝に残っています。

枝をmainへ合流させる

$ git merge fix-menu-price
Updating 84b3ca8..9033672
Fast-forward
 menu.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

git merge は、指定した枝を今いる枝へ取り込みます。先にmainへ移ってから打つのが決まりです。

終わった枝を消す

$ git branch -d fix-menu-price
Deleted branch fix-menu-price (was 9033672).

$ git log --oneline --graph
* 9033672 (HEAD -> main) 料金表を修正
* 84b3ca8 (origin/main) ホームページの最初の版を登録

枝の札を外しただけなので、コミットはmainに残っています。最後に git push を打てば、GitHubにも届きます。

fast-forwardと3-wayマージ

合流には2通りあります。先ほどの表示の「Fast-forward」は、mainが枝を切ったときのまま動いていなかった場合で、札を先へ進めるだけで終わります。

一方、枝で作業している間にmainも進んでいると、札を動かすだけでは合流できません。割り込みの話を再現します。add-summer-class で「夏の教室を追加」をコミットし、そのあとmainに戻って「月謝を修正」をコミットした状態です。

$ git log --oneline --graph --all
* c5ec49a (HEAD -> main) 月謝を修正
| * 51809aa (add-summer-class) 夏の教室を追加
|/  
* 9033672 料金表を修正
* 84b3ca8 (origin/main) ホームページの最初の版を登録

線が枝分かれしています。左の列がmain、右の列が枝です。|/ の下で2本に分かれたと読みます。ここで合流させます。

$ git merge add-summer-class
Merge made by the 'ort' strategy.
 index.html | 1 +
 1 file changed, 1 insertion(+)

$ git log --oneline --graph
*   a5e02a9 (HEAD -> main) Merge branch 'add-summer-class'
|\  
| * 51809aa (add-summer-class) 夏の教室を追加
* | c5ec49a 月謝を修正
|/  
* 9033672 料金表を修正
* 84b3ca8 (origin/main) ホームページの最初の版を登録

a5e02a9 が新しくできた合流用のコミットで、マージコミットと呼びます。この場合はメッセージを書くエディタが開くので、既定の文言のまま保存して閉じれば進みます。ort はGitの合流の方式の名前で、気にしなくて構いません。

fast-forward3-wayマージ
起きる条件mainが枝を切ったときのままmainも枝も進んでいる
何をするか札を先へ進めるだけ2つの枝と共通の元を比べて合わせる
新しいコミットできないマージコミットが1つできる
履歴の形1本のまま分かれて合流した形が残る

3-wayの「3」は、2つの枝の先頭と、分かれる前の共通の元の3点を比べるという意味です。同じ行を両方で変えていなければ、Gitが自動で合わせます。

fast-forwardと3-wayマージの違い
fast-forwardと3-wayマージの違い

git checkout と git switch の関係

古い記事やAIの回答には git checkout がよく出てきます。もともと git checkout は、枝の切り替えとファイルを戻す操作の両方を担っていました。役割が2つあって分かりにくいので、Git 2.23で git switch と git restore に分けられました。

やること新しい書き方古い書き方
枝を作って移るgit switch -c fix-menu-pricegit checkout -b fix-menu-price
枝を移るgit switch maingit checkout main
編集を捨てて戻すgit restore menu.htmlgit checkout -- menu.html

3行目は、コミットしていない編集を捨てる操作です。捨てた編集はGitから取り戻せません。

git checkout もなくなったわけではなく、今も動きます。実際に打つと、表示は同じです。

$ git checkout -b add-summer-menu
Switched to a new branch 'add-summer-menu'

$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

自分で打つときは、git switch のほうが誤解がありません。

GitHub Flow・Git Flow・GitLab Flowの違い

枝の使い方の決まりごとには、有名な型が3つあります。

GitHub FlowGit FlowGitLab Flow
残し続ける枝main だけmain(原案の名前は master)と developmain と、必要に応じて本番用や検証用の枝
作業の枝短い枝を1本ずつ機能・リリース準備・緊急修正で別々の枝機能ごとの枝
合流先プルリクエストで main へふだんは develop、リリース時に main へmain へ入れ、検証用を経て本番用へ
向いている場面更新をすぐ出すサイトやツール版番号を付けて出す製品、複数の版を保守する製品本番の前に検証環境を挟みたいとき

小さな会社はGitHub Flowで足ります。公式の手順は次の6つです。

  1. 枝を作る。
  2. 変更してコミットし、pushする。
  3. プルリクエストを作って見てもらう。
  4. 指摘があれば追加でコミットする。
  5. 承認されたら main へmergeする。
  6. 枝を消す。

Git Flowは2010年に提案された型で、提案した本人が2020年に、継続して更新を出すソフトならもっと単純な型を勧める、という注記を書き足しています。その例に挙げているのがGitHub Flowです。枝が5種類あるGit Flowを、サイト1つと予約メールのスクリプトだけの会社が持つ理由はありません。「開発は develop で」と外注先に言われたら、Git Flowかそれに近い運用だと分かれば十分です。

GitLab Flowは、GitLabが公式に説明している型で、Git Flowより単純な代案という位置づけです。GitHubを使っていても、検証環境を別に持ちたいときは考え方を借りられます。

GitHub Flow・Git Flow・GitLab Flowの枝の形
GitHub Flow・Git Flow・GitLab Flowの枝の形

枝の名前の付け方

GitHubの公式は、短くて内容の分かる名前を勧めています。何をしている枝かが一覧で読めれば十分です。実際に運用していると、先頭に fix-(直す)や add-(足す)を付けて、英小文字とハイフンで書く形が迷いません。

悪い例良い例理由
testfix-menu-price何の枝か分かる
daihyo-1107add-summer-class人と日付はコミットに記録されている
fix menu pricefix-menu-price空白は使えない

終わった枝の消し方

合流の終わった枝を残すと、どれが作業中か分からなくなります。GitHub Flowの手順にも、最後に枝を消すとあります。枝は手元とGitHubの両方にあるので、それぞれ消します。

$ git push -u origin fix-menu-price
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 11 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 375 bytes | 375.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
To ../remote/website.git
 * [new branch]      fix-menu-price -> fix-menu-price
branch 'fix-menu-price' set up to track 'origin/fix-menu-price'.

$ git branch -d fix-menu-price
Deleted branch fix-menu-price (was 495aae7).

$ git push origin --delete fix-menu-price
To ../remote/website.git
 - [deleted]         fix-menu-price

上の3つの間で、mainへの合流とpushは済ませています。GitHubの画面なら、マージ済みのプルリクエストの下のほうにある「Delete branch」でも消せます。消した枝は、同じ画面の「Restore branch」で戻せます。

Bが消した枝の控えが手元に残っているときは、git fetch --prune で片づきます。

$ git fetch --prune
From ../remote/website
 - [deleted]         (none)     -> origin/fix-phone-number
マージのあとに出る Delete branch のボタン
マージのあとに出る Delete branch のボタン

できないことと、その代わり

できないこと代わりにどうするか
1つのフォルダで2つの枝の中身を同時に開くgit switch で切り替える。並べて見たいなら別のフォルダにもう1つcloneする
作っただけの枝をGitHubに出すgit push -u origin 枝の名前 で送る
枝を消して、合流済みの変更を取り消す枝を消してもコミットは残る。「Gitで元に戻す:revert・reset・restoreの違いと使い分け」へ
同じ行の変更を自動で合流する人がどちらかを選ぶ。「コンフリクトの直し方:起きる理由と、解消の手順」へ

よく詰まるところ

not fully merged で枝が消えない

$ git branch -d try-new-design
error: the branch 'try-new-design' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D try-new-design'
hint: Disable this message with "git config set advice.forceDeleteBranch false"

まだ合流していないコミットがある枝です。Gitが消すのを止めてくれています。合流が先なら git merge を、本当に捨てるなら大文字の git branch -D try-new-design を打ちます。-D で消すと、その枝にしかないコミットは見えなくなります。

used by worktree で枝が消えない

$ git branch -d try-new-design
error: cannot delete branch 'try-new-design' used by worktree at '/private/tmp/…/gitlab-11/website'

今いる枝は消せません。途中の長いフォルダ名は縮めました。git switch main で移ってから消します。

枝の名前でエラーになる

$ git switch -c "fix menu price"
fatal: 'fix menu price' is not a valid branch name
hint: See `man git check-ref-format`
hint: Disable this message with "git config set advice.refSyntax false"

$ git switch fix-menu-price
fatal: invalid reference: fix-menu-price

$ git switch -c fix-menu-price
fatal: a branch named 'fix-menu-price' already exists

別々の場面の表示を並べました。1つ目は空白が入っていて使えない名前、2つ目はまだない枝へ移ろうとした(-c の付け忘れか、打ち間違い)、3つ目は同じ名前の枝がすでにあるという表示です。3つ目は -c を外して移ります。

枝を移ろうとすると overwritten と出る

$ git switch fix-trial-price
error: Your local changes to the following files would be overwritten by checkout:
	menu.html
Please commit your changes or stash them before you switch branches.
Aborting

未コミットの編集が、移り先の枝の内容とぶつかる場合に止まります。編集は消えていません。先にコミットするか、git stash でよけてから移ります。ぶつからないときは、編集を持ったまま移れます。別の枝に持ち込んでしまう原因になるので、移る前に git status を見る癖をつけると安全です。

mainに直接コミットしてしまった

枝を切るつもりだったのに、mainにコミットしてしまった場合です。pushしていなければ、枝に移し替えられます。

$ git branch fix-menu-title

$ git reset --hard origin/main
HEAD is now at a5e02a9 Merge branch 'add-summer-class'

$ git log --oneline --graph --all -4
* 1181a88 (fix-menu-title) 料金表の見出しを修正
*   a5e02a9 (HEAD -> main, origin/main) Merge branch 'add-summer-class'
|\  
| * 51809aa 夏の教室を追加
* | c5ec49a 月謝を修正
|/  

先に枝の札を今の位置に付け、mainの札をGitHub側の位置まで戻しています。git reset --hard は未コミットの編集を捨てる操作なので、git status が空なのを確かめてから打ちます。pushしたあとは、この手順を使いません。

mergeで CONFLICT と出た

$ git merge fix-tool-a
Auto-merging menu.html
CONFLICT (content): Merge conflict in menu.html
Automatic merge failed; fix conflicts and then commit the result.

両方の枝で同じ行を変えています。壊れたわけではなく、やめるなら git merge --abort で合流の前に戻ります。

次に読む

枝の変更をGitHubの上で見てもらう流れは「プルリクエストとは:作り方・レビュー・マージの流れ」、合流でぶつかったときは「コンフリクトの直し方:起きる理由と、解消の手順」にあります。

コミットとpushから確かめたいときは「git add・commit・pushの違い:保存して送るまでの流れ」、他の人の枝を受け取るときは「git clone・pull・fetchの違い:受け取る操作の使い分け」へ。

運用の決めごとは「Git FlowとGitHub Flowの違い:小さなチームはどちらで回すか」、枝を切り替えるコマンドの違いは「git switchとcheckoutの違い:枝を切り替える操作と、detached HEAD」にあります。

あわせて読む