Gitのブランチ|いま自分がどこにいるかを確かめてから切り替える

git ブランチで検索して出てくる説明は、たいてい「作業を枝分かれさせる仕組み」という比喩から始まります。比喩は分かるのに手が止まるのは、画面に出ている一覧のどこを見れば「いま自分がどこにいるのか」が分かるのかを、誰も教えてくれないからです。ここでは一覧の読み方から入り、作っても移動しない理由、切り替えが断られる条件、消してよい枝の見分け方までを順に整理します。

ブランチは、コミットを指す名札にすぎない

ブランチを「作業のコピー」だと思っていると、操作の結果が予想と合わなくなります。実体はもっと薄く、特定のコミットを指している名札です。そして、その名札のうちどれを使っているのかを指し示しているのが HEAD という別の目印です。この二段構えを押さえると、切り替えの動きがそのまま読めます。

実際には、git checkout ブランチ名 のようにブランチ指定で移動することが多く、その場合、HEADのポインタはブランチを指しています。この時、HEADはそのブランチが参照しているコミットを間接的に参照している状態になっています。なので、そのブランチで新しいコミットがあると、ブランチの参照先が切り替わり、同時にHEADの参照先も同時に切り替わります。 出典: qiita.com

コミットするたびに名札のほうが前へ進み、HEAD はその名札を指したままになります。だからブランチ上で作業している間は、自分の位置を気にせずコミットを積めます。逆に、名札ではなくコミットを直接指した状態になると、コミットを積んでも前へ進む名札がありません。この状態を切り離された HEAD と呼び、公式のマニュアルは、そこで作ったコミットは HEAD からしか参照されておらず、参照を作らないままにしておくと通常のごみ集めの処理でいずれ消えると説明しています。枝分かれの図よりも、この一文のほうが実務では役に立ちます。

一覧の色と記号が、そのまま状態の報告になっている

git branch を引数なしで打つと名前が並び、そのうち1つに星印が付きます。ここで見るべき情報は2つあり、片方はほとんど知られていません。公式のマニュアルは、いま使っているブランチが緑で表示されて星印が付き、連結された作業ツリーで使われているブランチは水色で表示されてプラス記号が付くと書いています。

プラス記号は「同じ機械の別の作業ツリーで、その枝が開かれている」という意味です。この印が付いた枝に対しては、Git がいくつかの操作を断ります。強制の指定を足しても、同じリポジトリに連結された別の作業ツリーで使われている枝は動かせません。記号の意味を知らないと、この拒否は不具合のように見えます。

一覧を機械に読ませたいときは、星印を探す必要はありません。git branch --show-current はいま使っている枝の名前だけを出します。ここで大事なのは、切り離された HEAD の状態では何も出力されないことです。空の出力は失敗ではなく、指しているのがコミットで名札ではない、という正しい答えです。

-v を足すと、枝ごとに短縮されたコミットの名前と件名、上流の枝との進み遅れが並びます。短縮の桁数は既定で7桁で、core.abbrev の設定で変えられます。-v を2つ重ねると、連結された作業ツリーの場所と上流の枝の名前まで出ます。ここでも読み方に癖があり、いま作業している側の場所は出力されません。場所が空いている行が、自分がいる作業ツリーです。

作っただけでは、移動しない

最初につまずくのはここです。git branch 名前 は新しい名札を作りますが、作業ツリーをそちらへ移しません。公式のマニュアルもその点をはっきり書いていて、この操作は枝を作るが作業ツリーは切り替えない、と明記しています。

つまり git branch feature-x を打った直後も、自分はさっきまでの枝の上にいます。そのまま次のコミットを積むと、新しく作った枝ではなく元の枝に入ります。「枝を切ったつもりなのに、履歴が本流に積まれていた」という事故のほとんどはこれです。

同じマニュアルは、そのまま移動したい場合の近道も書いています。すぐ切り替えたい枝を作るなら、git switch に -c を付けて1つのコマンドで済ませるほうが簡単だという案内です。古い解説では git checkout -b が同じ役割で紹介されていて、こちらも動きます。違いは、git checkout がファイルの書き戻しまで兼ねている点にあります。枝の操作とファイルの操作を分けたい場面では、git switch を選ぶほうが、ファイル名を渡したつもりで枝を切り替える事故が起きません。

枝を切る位置にも指定の幅があります。何も書かなければ現在の位置から、コミットの名前やタグを書けばそこから始まります。A...B と書くと、2つの履歴が分かれた地点から切れます。レビュー用に「分岐した地点だけを再現したい」ときに使う書き方です。

最初の枝の名前は、いまも master のまま

新しくリポジトリを作ったときの枝の名前は、環境によって master と main が入り混じっています。これは流儀の違いではなく、既定値と設定の差です。公式のマニュアルは、指定しなかった場合の名前は現時点では master であり、Git 3.0 の公開時に main へ変わると書いています。名前は init.defaultBranch の設定で変えられ、作るときに git init -b 名前 と書いて指定することもできます。

手元が master、共有先が main という食い違いは、最初の送信で気付くのが一番遅い形です。先に設定を合わせておくと、送り先の枝を作り直す手間が消えます。共同で使うリポジトリがある場合は、その名前に合わせておくのが後の手数を減らす選択です。

名前そのものの決まりも、自由ではありません。枝の名前は Git の参照名の検査を通る必要があり、使える文字に制限があります。空白や連続したドットを含む名前が弾かれるのはこの検査によるもので、命名の自由度を試すより、記号を使わない短い名前にしておくほうが扱いやすくなります。

リモートの枝と繋がるのは、名前ではなく設定

手元の枝と送り先の枝は、名前が同じだから対応しているわけではありません。branch.<名前>.remote と branch.<名前>.merge という2つの設定が書かれていて、初めて対応します。ここを誤解していると、引数なしの取得や送信が通る枝と通らない枝が混ざる理由が分かりません。

書かれるかどうかを決めているのは branch.autoSetupMerge の設定で、既定は true です。この値のとき、切った元がリモート追跡の枝であれば自動で設定が入ります。手元で新しく作った枝は元が手元の枝なので設定が空で、最初の1回だけ送り先を明示することになります。

設定の値 自動で設定が入る条件
false 入らない
true 切った元がリモート追跡の枝のとき(既定)
always 切った元が手元の枝でもリモート追跡の枝でも入る
inherit 切った元の設定をそのまま引き継ぐ
simple 切った元がリモート追跡の枝で、名前も同じときだけ

すでにある枝に後から設定を足すなら --set-upstream-to、短い形では -u です。外すなら --unset-upstream です。この設定が入っていると、状態の確認や一覧に進み遅れの件数が出るようになり、引数なしの取得がどこから取るかを判断できるようになります。

消してよい枝は、機械に選ばせる

溜まった枝を目で選ぶ作業は、判断を間違えたときの損が大きい割に退屈です。公式のマニュアルは、似ているが目的の違う4つの絞り込みを並べていて、これを使えば選別は機械の側に寄せられます。

絞り込み 一覧に出るもの 何のために使うか
--merged HEAD に完全に含まれている枝 安全に消せる枝を見つける
--no-merged HEAD に完全には含まれていない枝 これから取り込む候補を見つける
--contains コミット そのコミットを含む枝 履歴を組み替えたときに手当てが要る枝を見つける
--no-contains コミット そのコミットを含まない枝 上の逆

比べる相手を書かなかった場合、基準は HEAD になります。つまり、いま自分がいる枝を基準に判定します。同じコマンドが日によって違う答えを返すのはこれが理由で、本当に聞きたいのが本流に取り込まれたかどうかであれば、基準となる枝の名前を書いたほうが正確です。

消す側の指定は2種類あります。-d は安全側で、上流の枝に取り込まれていること、上流が設定されていなければ HEAD に取り込まれていることを条件にします。-D は強制付きの短縮形で、取り込まれていない枝も、正しいコミットを指していない枝も消せます。どちらの場合も、枝に付いていた変更の記録も一緒に消えます。-D を打つ前に一度 --no-merged を見ておくと、取り戻せない捨て方を避けられます。

リモート追跡の枝を消すには -r を -d と組み合わせます。ここには注意書きが付いていて、この操作が意味を持つのは相手のリポジトリから枝が消えている場合か、取得の設定で取ってこないようにしてある場合だけです。相手にまだ残っていれば、次の取得で戻ってきます。すでに相手から消えた枝をまとめて片付けるなら、git remote の prune を使うほうが速く済みます。

切り替えが断られたときに選べる3つの道

編集を抱えたまま枝を移ろうとすると、Git が断ることがあります。断る条件は決まっていて、編集したファイルの中身が、いまの枝と移り先の枝で違っている場合です。公式のマニュアルは、その編集を文脈ごと保つために切り替えを断ると説明しています。中身が変わっているファイルの上に編集だけを運ぶと、誰も書いていないものができてしまうからです。

道は3つあります。1つは、途中でもコミットしてしまう方法です。後でまとめ直せるので、失うものがありません。2つめは git stash で編集を一時的に預ける方法で、移った先で取り出せます。3つめは切り替えに --merge を付けて、いまの枝と作業中の内容と移り先の3方向で統合させる方法です。ただしこの道には、切り替え時に段取り済みの内容が失われることがある、という注意書きが付いています。

-f は3つのどれとも違います。これは指定した先へ強引に移る指定で、作業ツリーの編集も、邪魔になっている追跡外のファイルも捨てます。追跡外のファイルは一度も Git に記録されていないため、捨てたあとに取り戻す手段がありません。同じ「強制」の語でも、取り返せるかどうかがここで分かれます。

手が止まっているのは、コマンドではなく窓の行き来

枝の操作でつまずいた場面を並べると、コマンドを知らないから止まっているものは意外に少数です。多いのは次の3つです。

  • いまどの枝にいるのか分からないまま編集を始めている
  • 一覧に出ている名前と、手元のフォルダの中身が頭の中で繋がらない
  • 差分を読むために別のアプリを開いた結果、元の作業に戻れない

3つとも、覚えるコマンドを増やしても解けません。効くのは、枝の名前とファイルの並びが同じ画面に見えている状態です。フォルダとターミナルとAIが同じ窓にある形にすると、いま開いているフォルダに対してそのままコマンドを打てるので、位置を取り違えたまま操作する場面が減ります。同じ画面に何が収まるのかはできることに整理してあり、すでに使っているファイル管理の道具との違いは他のファイル管理との比較にまとめてあります。

長く動くエージェントに枝の操作を任せているときは、席を外している時間のほうが長くなります。移動中に手元の端末から同じ画面を見て返す使い方はiPhone・iPadから続きをにあります。無料で試せる範囲と費用の考え方は料金に、導入前に多い質問はよくある質問に置いてあります。

枝の扱いで押さえる順番は、いまどこにいるかを読む、作ってから移る、繋ぎ先を設定する、消してよいものを機械に選ばせる、の4段です。この順に手を入れると、覚えるコマンドの数は増えないまま、手が止まる回数が減ります。

よくある質問

git branch で名前の前にプラス記号が付いているのは何ですか?

その枝が、同じ機械の別の作業ツリーで使われているという印です。公式のマニュアルは、連結された作業ツリーで使われている枝は水色で表示されてプラス記号が付くと書いています。この状態の枝は、強制の指定を足しても動かせないため、拒否の理由が分からないときは最初にこの記号を確かめると早く済みます。

枝を作ったのに、コミットが元の枝に入ってしまうのはなぜですか?

作る操作と移る操作が別だからです。公式のマニュアルも、枝は作られるが作業ツリーは切り替えられないと明記しています。作ってすぐ移りたい場合は git switch -c 名前 のように、1つのコマンドで両方を済ませる書き方を使うと取り違えが起きません。

新しいリポジトリの最初の枝が master になるのは古い設定ですか?

古い設定ではなく、いまの既定値です。公式のマニュアルは、指定しなかった場合の名前は現時点では master であり、Git 3.0 の公開時に main へ変わると書いています。手元と共有先で名前を揃えたい場合は init.defaultBranch を設定するか、作るときに git init -b 名前 で指定します。

消してよい枝を安全に選ぶ方法はありますか?

--merged で、基準に完全に含まれている枝だけを一覧にできます。公式のマニュアルはこの絞り込みを、安全に消せる枝を見つけるためのものと説明しています。基準を書かないと現在の枝が基準になるため、本流に取り込まれたかどうかを知りたいときは、その枝の名前を明示して打つほうが正確です。

記事一覧へ戻る