はじめに
「mainブランチの変更を個人ブランチに取り込んで」
初めてこの指示を受けたとき、すごく困った記憶があります。
なぜなら、今までmain1本のブランチ構成しか触ったことがなく、操作のほとんどがGUIによるものであり、CLIのgitコマンドもなんとなくでしか触ってこなかったからです。
gitコマンドは引数の省略が可能なこともあり、何を省略しているのかを理解しないままに叩くと、余計に理解のできない状態を生み出してしまいます。
後で見返せるようにしたいなという思いもあり、いわゆる"取り込み"で使用するgitコマンドについて整理し、この順番でCLIに貼り付ければok! な備忘録を作成してみました。
同じような壁にあたった方の役に少しでも立てたら嬉しいです。
忙しい人向け(結論)
mainの変更をfeature/xxx999に取り込むには、以下を順に実行します。
(なぜこの順番なのかは本文で解説します)
git fetch origin main
git switch main
git merge --ff-only origin/main
git rebase main feature/xxx999
本文
本文では、用語の定義,前提条件を整理したのちに、使用するコマンドの整理を行い、結論を出します。
用語の定義
- 個人ブランチは以後、
feature/xxx999と言い換える - リモートリポジトリのmainブランチのことを、
origin mainと呼称する -
origin mainに対応するリモート追跡ブランチのことを、origin/mainと呼称する -
origin/mainは最後にfetchしたときの、origin mainの状態を指すポインタである - ローカルリポジトリのmainブランチのことを
mainと呼称する
前提条件
実際のコマンドの話をする前に、前提条件について整理します。
はじめにで書いたように、ブランチ構成が変わると、必要なコマンドも変化するためです。
ブランチ構成
今回の例では、GitHub Flowというブランチ構成を例とします。
具体的には以下のような構成です。
- main
- feature/xxx999
- feature/xxx999
feature/xxx999のxxx999は、任意の文字列かつ,複数のfeature/xxx999が存在しうることを表現しています。
ブランチ運用ルール
ブランチ運用ルールは以下のとおりとします。
-
mainに直接変更を加えてはならない -
mainからorigin mainに、pushしてはならない -
feature/xxx999からorigin mainに、pushしてはならない -
feature/xxx999から、対応するorigin feature/xxx999には、push可能である -
origin feature/xxx999からorigin mainへpull requestを依頼することで、origin feature/xxx999の変更をorigin mainに反映する -
origin feature/xxx999の変更がorigin mainに反映されたなら、対応するfeature/xxx999は新規コミットを積まずに削除する
条件から導出した実態
これらの前提条件から、以下のことが言えます。
-
変更の反映の流れは、1方向にループしており、
main->feature/xxx999->origin feature/xxx999->origin main->main
のような流れである -
mainに直接変更を加えてはいけないルールと、上述の1方向性から、mainのコミットの全てをorigin mainが知っている状態である
使用するコマンドの整理
ここでは、まずmainブランチ1本の構成の場合に変更を取り込むコマンドを確認したのちに、使用しているコマンドが引数になにをとるのか知ることで、mainブランチの変更を、featureブランチに取り込むコマンドを理解します。
mainブランチ1本の構成の場合に変更を取り込むコマンド
mainブランチ1本の場合に変更を取り込むコマンドは以下のとおりとします。
git fetch origin
git rebase origin/main
git pullについては当記事では言及しません。
次に、git fetchが引数に何をとるか知ることで、origin mainの変更をfeature/xxx999に取り込む場合にどう変化するかを理解します。
git fetch
構文
git fetch origin main
↑ ↑
引数1 引数2
(repository) (refspec)
-
repositoryには、リモートリポジトリ名をとる。省略した場合、現在のブランチに設定された追跡リモート(なければorigin)が使われる。複数リモートを扱わない限りは意識しなくてよい -
refspecには、リモートリポジトリのブランチ名をとる。省略した場合、通常は全ブランチを対象となる
refspecは本来 <src>:<dst> という形式で「どのブランチを取得し、どのローカル追跡ブランチを更新するか」を指定します。
mainのようにブランチ名だけ書いた場合は main:(取得元だけ指定)の短縮形になります。
取得先を書かなくても、取得した内容は対応するリモート追跡ブランチ(今回の場合はorigin/main)に記録されます。
引数の省略
git fetchは引数1,引数2のいずれも省略することができ、引数の数に応じて以下のように動作します。
| コマンド | 動作 |
|---|---|
git fetch |
現在のブランチの追跡リモート(なければorigin)の全ブランチを取得 |
git fetch origin |
origin の全ブランチを取得 |
git fetch origin main |
origin main だけ取得 |
今回実行するコマンド
以上の内容により、origin mainの更新を、origin/mainに取り込む場合、以下のコマンドを実行します。
git fetch origin main
次に、git rebaseが引数に何をとるか知ることで、origin mainの変更をfeature/xxx999に取り込む場合にどう変化するかを理解します。
git rebase
構文
git rebase origin/main main
↑ ↑
引数1 引数2
(upstream) (branch)
-
upstreamには、commit-ish(リモート追跡ブランチまたは,ローカルブランチなど)を引数にとる。省略した場合、現在のブランチに設定された追跡ブランチとなる -
branchには、rebaseの対象となるローカルブランチ名をとる。省略した場合、現在のブランチとなる
commit-ishは当記事で扱う範囲に絞って例示しています。
本来commit-ishがとりうる範囲から、大幅に絞って例示していることをご理解ください。
第2引数branchを渡すと rebase 前に自動でgit switch branchされ、終了後もそのブランチに留まります。
例えばgit rebase origin/main mainを実行した場合、現在ブランチがmainに変わります。
引数の省略
git rebaseは引数1,引数2のいずれも省略することができ、引数の数に応じて以下のように動作します。
| コマンド | 動作 |
|---|---|
git rebase |
現在のブランチに設定されたupstream(のリモート追跡ブランチ)をもとに、現在のブランチをrebaseする |
git rebase main |
mainをもとに、現在のブランチをrebaseする |
git rebase origin/main main |
origin/mainをもとに、mainをrebaseする |
git rebaseを引数なしで実行する場合、現在のブランチにupstreamが設定されていないとエラーで中止されます。
今回実行するコマンド
以上の内容により、origin mainの更新を、origin/mainに取り込む場合、git rebaseを2回実行することになり、それぞれ以下のコマンドが対応します。
origin/mainの更新を、mainに取り込む場合
git rebase origin/main main
mainの更新を、feature/xxx999に取り込む場合
git rebase main feature/xxx999
既にpush済みのfeature/xxx999を、rebase後にorigin feature/xxx999へ上げ直す際は、履歴が書き換わっているためgit push --force-with-leaseが必要になります。
しかし、当記事では取り込みまでをスコープとし、push以降には言及しません。
ところで、今回のブランチ運用ルールに基づいた場合、git rebase origin/main mainは、再適用すべきコミットが存在しないため、結果的にmainがorigin/mainの位置まで進むだけになります。これはfast-forwardと同じ到達点です。
fast-forward とは
ブランチB(origin/main)の先端コミットの履歴をたどると、ブランチA(main)の先端コミットに到達できる(=AがBの祖先である)とき、AをBへfast-forwardできると言います。
このとき分岐が無いため、AのポインタをBの先端まで進めるだけで統合が完了します。
言葉が難しいので噛み砕くと、あるブランチAの全てのコミットを、別のブランチBが知っている状態 と言えるでしょう。
今回のブランチ運用ルールでは、mainに独自のコミットを作らないため、mainの全てのコミットは常にorigin/mainに含まれています。つまりorigin/mainはmainの全コミットを知っており、mainは常にorigin/mainへfast-forward可能な状態に保たれているということになります。
結果、git rebase origin/main mainは、mainの最終的な到達コミットという意味で、以下のコマンドと同じ結果になります。
git merge --ff-only origin/main
厳密には、rebaseは「コミットの付け替え」操作であり、merge --ff-onlyの「ポインタの前進」とは仕組みが異なります。
あくまで「独自のコミットが無い前提での到達点が同じ」という意味です。
git rebaseを使用するのではなく、git merge --ff-onlyを使用するメリットは、fast-forwardによる取り込みであることを保証できる点です。
(fast-forwardできない場合はエラーメッセージを出してmergeが中止されるため、もしmainに意図せず独自コミットが入ってしまっていた場合、それに気づける。)
念のため、git merge引数に何をとるかを知っておきましょう。
git merge
構文
git merge --ff-only origin/main
↑ ↑
オプション 引数1
(commit)
-
commitには、commit-ish(リモート追跡ブランチまたは,ローカルブランチなど)を引数にとる。省略した場合は現在のブランチに設定されたupstream(のリモート追跡ブランチ)がマージ対象となる
複数のcommit-ishを引数にとるmergeについては当記事では言及しません。
引数の省略
git mergeは引数1を省略することができ、引数の数に応じて以下のように動作します。
| コマンド | 動作 |
|---|---|
git merge |
設定されたupstreamのリモート追跡ブランチを現在のブランチにマージする |
git merge origin/main |
origin/main を現在のブランチにマージする |
git mergeを引数なしで実行する場合、現在のブランチにupstreamが設定されていないとエラーで中止されます。
結論
mainブランチの変更を、featureブランチに取り込む場合、以下のコマンドを実行します。
origin mainの変更を、origin/mainに反映する。
git fetch origin main
mergeをmainに対して実行するため、明示的にブランチを移動。
git switch main
origin/mainからmainに対して、fast-forwardによるmergeを実行。
git merge --ff-only origin/main
rebaseをfeature/xxx999に対して実行するため、明示的にブランチを移動。
git switch feature/xxx999
mainからfeature/xxx999に対して、rebaseを実行。
git rebase main feature/xxx999
git fetch origin main
git rebase origin/main feature/xxx999
の順で実行すればswitchもmergeも省略できますが、switch -cで新規にfeature/xxx999を作成することを考慮し、省略していません。
おわりに
普段何気なく使っているgitコマンドを整理してみたいなと思い、初めて筆をとってみましたが、想定以上に長く、踏み込んだ内容になってしまいました。
なるべく確認しながら進めたつもりではありますが、内容に誤りがあれば是非ご教授いただけますと幸いです。