Git入門:バージョン管理のきほん

ブランチとは何か

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部
ブランチは並行世界

このレッスンで分かること

  • ブランチはフォルダのコピーではなく、コミットを指す付箋にすぎないこと
  • HEAD とブランチの関係、コミットするとブランチが進む仕組み
  • ブランチの一覧・作成・削除と、消したときに何が起きるか

ブランチはフォルダのコピーではない

ブランチの説明で「作業を枝分かれさせる」「並行して開発できる」と聞くと、多くの人は次のように想像します。プロジェクト一式がもう 1 セット複製されて、そちらで作業する。だからブランチを作るのは重い操作で、たくさん作ると容量を食う。

これは全部違います。ブランチの実体は、1 個のコミットのハッシュ値が書かれただけの小さなファイルです。

実際に中を見てみます。

ターミナル

$ cat .git/refs/heads/main 8f3c1a2b7d4e9c0a5f2b8e6d3c1a4f709b2e5d8c

これだけです。41 バイト(ハッシュ値 40 文字と改行)しかありません。ブランチとは「このコミットを指す付箋」であって、それ以上のものではありません。

git branch でブランチを作ったときに何が起きるかも、同じように確認できます。

ターミナル

$ git branch feature/search $ cat .git/refs/heads/feature/search 8f3c1a2b7d4e9c0a5f2b8e6d3c1a4f709b2e5d8c

同じハッシュ値が書かれたファイルが 1 個増えただけです。ファイルのコピーも、ディレクトリの複製も起きていません。1 万ファイルあるプロジェクトでも、作成は一瞬で終わります。

この「付箋」というモデルを覚えておいてください。マージも rebase も cherry-pick も、結局は「どのコミットからどのコミットへ、付箋をどう動かすか」の話に還元できます。ここを絵として理解しないまま操作を暗記すると、少し外れた状況で必ず詰まります。

コミットの連なりと、そこに刺さる付箋

Git の履歴は、コミットが親をたどる形でつながった鎖です。各コミットは「ひとつ前のコミット」を覚えています。

プレーンテキスト

8f3c1a2 ── 3e8b2d5 ── 9a1f4c7

いちばん右の 9a1f4c7 が最新です。ここに main という付箋が刺さっているとします。

プレーンテキスト

8f3c1a2 ── 3e8b2d5 ── 9a1f4c7 ← main

ここで git branch feature/search を実行すると、同じコミットにもう 1 枚付箋が増えます。

プレーンテキスト

8f3c1a2 ── 3e8b2d5 ── 9a1f4c7 ← main ← feature/search

この時点で、2 つのブランチの中身はまったく同じです。違いはありません。

ここで、よくある誤解をひとつ解いておきます。main は特別なブランチではありません。Git から見れば、mainfeature/search も同じ付箋です。main を大事に扱うのは、チームがそう決めているからであって、Git が優遇しているわけではありません。実際、git init で最初に作られるブランチの名前は設定で変えられます。

ターミナル

$ git config --global init.defaultBranch main

HEAD は「今どの付箋にいるか」

もうひとつ HEAD という目印があります。HEAD は「今いる場所」を指すもので、通常はコミットではなくブランチ名を指しています。

ターミナル

$ cat .git/HEAD ref: refs/heads/main

main というブランチを指している、という意味です。git switch でブランチを移ると、この中身が書き換わります。

ターミナル

$ git switch feature/search Switched to branch 'feature/search' $ cat .git/HEAD ref: refs/heads/feature/search

この状態でコミットすると、次のことが順に起きます。

  1. 新しいコミットが作られる。親は今のコミット(9a1f4c7
  2. HEAD が指しているブランチ、つまり feature/search が新しいコミットに進む
  3. main は動かない

プレーンテキスト

8f3c1a2 ── 3e8b2d5 ── 9a1f4c7 ← main └── 2b8f6a1 ← feature/search ← HEAD

枝分かれが生まれました。コピーはどこにも発生していません。増えたのはコミット 1 個と、付箋の移動だけです。

main に戻れば、作業ディレクトリのファイルは 9a1f4c7 の内容に戻ります。Git はブランチを移るたびに、指し先のコミットの内容に合わせて作業ディレクトリを書き換えているのです。「別のフォルダに移動している」のではなく「同じフォルダの中身を入れ替えている」と理解してください。

git switch の具体的な使い方はブランチを作って移動する(switch)で扱います。ここではまず、ブランチとは何かという仕組みだけを押さえてください。

この二段構えを押さえておくと、git log の表示も読めるようになります。

ターミナル

$ git log --oneline --graph --all * 2b8f6a1 (HEAD -> feature/search) 検索フォームを実装 * 9a1f4c7 (main) ログイン機能を追加 * 3e8b2d5 検索ボタンのスタイルを調整 * 8f3c1a2 初期コミット

かっこの中が、そのコミットに刺さっている付箋の一覧です。HEAD -> feature/search は「HEADfeature/search を指していて、そのブランチがこのコミットを指している」という意味です。--graph --all を付けると、枝分かれも線で描かれます。履歴が分からなくなったら、まずこれを打ってください。

ブランチを一覧・作成・削除する

コマンド何をするか
git branch手元のブランチを一覧する。* が今いるブランチ
git branch -v一覧に、指しているコミットとメッセージを添える
git branch <名前>今のコミットを指す付箋を作る。移動はしない
git branch <名前> <ハッシュ値>指定したコミットを指す付箋を作る
git branch -d <名前>ブランチを消す。取り込み済みでないと止まる
git branch -D <名前>取り込み済みでなくても強制的に消す
git branch -m <新しい名前>今いるブランチの名前を変える

一覧を見てみます。

ターミナル

$ git branch -v * feature/search 2b8f6a1 検索フォームを実装 main 9a1f4c7 ログイン機能を追加

* が付いているのが、今 HEAD が指しているブランチです。

削除は付箋をはがす操作です。コミットそのものは消えません。

ターミナル

$ git switch main $ git branch -d feature/search error: the branch 'feature/search' is not fully merged hint: If you are sure you want to delete it, run 'git branch -D feature/search'

-d は「まだどこにも取り込まれていないブランチ」を消そうとすると止まります。2b8f6a1main からたどれないので、付箋をはがすと誰からも指されないコミットになる。それを Git が警告しているわけです。

意図して消すなら -D を使います。

ターミナル

$ git branch -D feature/search Deleted branch feature/search (was 2b8f6a1).

(was 2b8f6a1) と、消した時点のハッシュ値が表示されます。これはメモしておく価値があります。あとで戻したくなったとき、この値からブランチを作り直せるからです。

ターミナル

$ git branch feature/search 2b8f6a1

ハッシュ値を控え忘れていても、消したはずの作業を取り戻す(reflog)の方法で探せます。

ブランチが軽いから、開発のやり方が変わる

ブランチが付箋 1 枚だと分かると、実務での使い方も変わります。

  • 作業のたびに切る。 機能ひとつ、不具合ひとつごとに新しいブランチを作ります。作成コストがほぼゼロなので、ためらう理由がありません
  • 試して駄目なら消す。 うまくいかなかったブランチは消せば済みます。main は一切汚れません
  • 危ない操作の前に目印を立てる。 git branch backup を打っておけば、その時点のコミットに付箋が残ります。resetrebase で失敗しても、そこへ戻るだけです

3 つ目は特に有効です。1 秒で打てて、事故のときの復旧が確実になります。

名前の付け方にも簡単な型があります。チームによりますが、次のように種類を頭に置くのが一般的です。

プレーンテキスト

feature/search-form fix/cart-empty-crash docs/setup-guide

スラッシュは階層の区切りに見えますが、Git 上はただの文字です。.git/refs/heads/feature/search-form という位置にファイルが作られるだけで、特別な意味はありません。

リモートのブランチも同じ付箋

GitHub 上のブランチも、仕組みは変わりません。origin はリモートリポジトリに付けられる既定の名前で、多くの場合 GitHub 上のリポジトリを指します。git fetch すると、リモートの状態を写した付箋が手元に作られます。

ターミナル

$ git branch -a * feature/search main remotes/origin/main remotes/origin/feature/search

remotes/origin/main は「最後に通信したとき、GitHub の main はここを指していた」という記録です。これも付箋であって、フォルダのコピーではありません。

大事なのは、この付箋が自動では動かないことです。同僚が GitHub に push しても、こちらの origin/main は古い場所を指したままです。git fetch を実行して初めて、最新の位置に付け替えられます。「GitHub では直っているのに手元では直っていない」と感じたときは、たいてい fetch をしていないだけです。この仕組みはclone と fetch の違いで詳しく扱います。

よくある失敗と、そこからの戻し方

いちばん多いのが、git branch で作っただけで移動したつもりになる失敗です。

ターミナル

$ git branch feature/search $ git add . $ git commit -m "検索フォームを実装する" [main 2b8f6a1] 検索フォームを実装する 2 files changed, 48 insertions(+)

[main 2b8f6a1] に注目してください。feature/search ではなく main にコミットされています。git branch は付箋を作るだけで、HEAD を動かさないからです。

main を汚してしまいましたが、ここまで読んだ内容だけで直せます。やることは付箋を動かすだけです。

まず、作ったブランチを今のコミットに合わせて作り直し、そちらへ移ります。

ターミナル

$ git branch -f feature/search $ git switch feature/search Switched to branch 'feature/search'

-f は既存のブランチを今のコミットに付け替える指定です。次に main を 1 個前に戻します。

ターミナル

$ git switch main $ git reset --hard HEAD~1 HEAD is now at 9a1f4c7 ログイン機能を追加 $ git log --oneline --all 2b8f6a1 検索フォームを実装する 9a1f4c7 ログイン機能を追加

コミットは feature/search に残り、main は元に戻りました。ファイルを移動したりコピーしたりは一切していません。付箋を 2 枚動かしただけです。

そもそも防ぐなら、作成と移動を同時にやる git switch -c を使ってください。

ターミナル

$ git switch -c feature/search Switched to a new branch 'feature/search'

Windows でも Mac でもコマンドは同じです。ただし、Windows のファイルシステムは大文字と小文字を区別しないので、Feature/Searchfeature/search が衝突することがあります。ブランチ名は小文字にそろえておくのが無難です。

この章のポイント
  • ブランチの実体はコミットのハッシュ値 1 行のファイル。フォルダのコピーではない
  • HEAD は今いるブランチを指す。コミットすると、HEAD が指すブランチだけが進む
  • 作成も削除もほぼコストゼロ。作業ごとに切り、危ない操作の前は git branch backup
  • git branch は作るだけで移動しない。作って移るなら git switch -c