Git の基本コマンドを仕組みで理解する― add・commit・branch・merge

前回までで、Git の中身は HEAD →ブランチ→コミット→ tree → blob という 5 段の鎖でできている、と確かめました。この鎖が頭に入ると、毎日使うコマンドはぐっと読みやすくなります。どのコマンドも、結局はこの鎖のどこかを動かしているだけだからです。

第 4 回は、その毎日のコマンドを扱います。addcommit、変更を見る diff、枝分かれの branchmerge、そして手元と GitHub をつなぐ clone / push / pull。コマンドの丸暗記はせず、「いま鎖のどこを動かしたのか」を見ていきます。AI に作業を任せるときも、この見方があれば「何を頼んでいるのか」がはっきりします。

この回からは、主なコマンドの下に「同じ操作を AI に頼むときのひと言」を添えます。どれも使い捨てのリポジトリで実際に AI へ渡し、意図どおりに動くことを確かめた言い方です。自分で打つコマンドと、AI に渡すひと言。両方を並べると、どちらで指示しても結局は同じ鎖を動かしているのだと見えてきます。

TL;DR

  • ファイルはコミットされるまで 作業ツリー→ステージング→リポジトリ の 3 つの領域を通ります。ステージング (買い物カゴ) があるから「何を 1 つの記録にするか」を選べます。
  • git diff は常に「3 つの領域のうち、どの 2 つを比べるか」。--stagedHEAD を付けると比較相手が変わります。
  • マージには 早送り(しおりをスライドするだけ) と 3 方向マージ(親が 2 つの新コミットを作る) があります。コンフリクトはエラーではなく、人間に判断を求める安全装置です。
  • clone / fetch / pull / pushリモート(GitHub のような共有の置き場) とやり取りします。fetchpull の違いは「合流までするか」です。

コミットまでに通る「3 つの領域」

git commit は一発で歴史に保存される、と思われがちですが、実際はその手前にもう 1 つ場所があります。Git でファイルは、コミットされるまでに 3 つの領域を通ります。

flowchart TD W["作業ツリー<br/>編集する場所"] -->|git add| S["ステージング<br/>コミットに入れる物を選ぶ"] -->|git commit| R["リポジトリ<br/>歴史として保存"]

スーパーでの買い物にたとえると分かりやすいです。作業ツリーは商品棚で、自由に手に取って見比べる場所。ステージングは買い物カゴで、「これは買う」と決めた物を入れておく場所。リポジトリはレジで、会計を済ませて記録が残る場所。git add がカゴに入れる操作、git commit がレジを通す操作です。

なぜわざわざカゴを経由するのでしょうか。今日 3 つの作業をして、完成したのは 2 つだけ、という場面を考えると腑に落ちます。カゴがあれば、完成した 2 つだけを入れてコミットし、作業中の 1 つは次回に回せます。「何をひとまとまりの記録にするか」を自分で選べる。これがステージングの役割です。

基本の流れはこうです。

git status # いまどのファイルがどの領域にあるか
git add README.md # 作業ツリー→ステージング(カゴに入れる)
git commit -m "ログイン画面を追加" # ステージング→リポジトリ(レジを通す)
git log # 残ったコミットの履歴を見る

AI に頼むなら

README.md だけをステージングして、『ログイン画面を追加』というメッセージでコミットして

このひと言で、AI は git add README.md から git commit までを順にこなします。ファイル名まで指定すれば、関係ないファイルを巻き込まず、狙ったものだけをカゴに入れてくれます。

コミットメッセージは、未来の自分への手紙です。何を変えたかは差分を見れば分かるので、メッセージには なぜ変えたか を残すのがコツです。

diff の「3 つの顔」

git diff は「変更を見せて」と AI に頼むときの裏側でもよく動くコマンドです。ただ、見せてくれる差分は「いま何と何を比べているか」で変わります。3 つの領域を思い出すと、その 3 つの顔がそのまま整理できます。

git diff # 作業ツリー vs ステージング(まだ add していない変更)
git diff --staged # ステージング vs 直前のコミット(add したがまだ commit していない変更)
git diff HEAD # 作業ツリー vs 直前のコミット(前回コミットからの全変更)

AI に頼むなら

まだ add していない変更だけ差分で見せて

と言えば git diff が、「add 済みでまだコミットしていない分を見せて」と言えば git diff --staged が選ばれます。どの 2 つの領域を比べたいかを言葉にすれば、AI が対応するオプションを補ってくれます。

git add した瞬間に git diff の出力が消えて戸惑うことがあります。これは変更が消えたのではなく、比較相手 (カゴ) が更新されて「作業ツリーとカゴが一致した」だけです。差分は常に「3 つの領域のうち、どの 2 つを比べるか」だと分かっていれば、慌てずにすみます。

枝分かれして、合流する

前回、ブランチの正体は「コミットを指す 40 文字のしおり」だと確かめました。だから枝分かれは一瞬です。

git switch -c login # login ブランチを作って移動
# …作業して add & commit…
git switch main # main に戻る
git merge login # login の成果を main に取り込む

AI に頼むなら

main はそのままに、login という名前で新しいブランチを作って移動して

ブランチ名まで言うのがコツです。名前を省いて「新機能用の別ブランチを作って」とだけ頼むと、AI は feature のような汎用名を勝手に見つくろうので、あとから何の枝か分からなくなります。「login」と一語添えるだけで、狙った名前の枝に乗れます。

合流 (マージ) には、状況によって 2 つのパターンがあります。

ひとつは 早送り (fast-forward) です。main が枝分かれ後に 1 歩も進んでいなければ、Git は新しいコミットを作りません。main のしおりを login の位置までスライドさせるだけで合流が終わります。

C1 ← C2 ← C3 ← C4	↑ ↑ main login → merge後、main も C4 を指す

もうひとつは 3 方向マージ です。mainlogin が両方とも独自に進んでいると、スライドでは追いつけません。Git は「枝分かれした地点」「main の先端」「login の先端」の 3 点を見比べ、両方の変更を 1 つにまとめた 新しいマージコミット(親が 2 つ) を作ります。

flowchart TD C1["C1"] --> C2["C2 分岐点"] C2 --> C3["C3 (main の先端)"] C2 --> C4["C4 (login の先端)"] C3 --> M["M マージコミット<br/>親が C3 と C4 の2つ"] C4 --> M

同じファイルの同じ行を、両方の枝で別々に変えていると、Git は「どちらが正しいか」を機械的に決められません。このときに起きるのが コンフリクト (競合) です。

実際に手元でわざと起こして直してみると、コンフリクトは怖くなくなります。第3回で作った git-lab(無ければ mkdir ~/Desktop/git-lab && cd ~/Desktop/git-lab && git init -b main で作れます) で、同じ行を別々に変えてぶつけてみます。

# 共通の出発点を作る
printf '納期: 未定\n' > schedule.txt
git add schedule.txt && git commit -m "納期ファイルを追加"
# feature 側は「7月末」に変える
git switch -c feature
printf '納期: 7月末\n' > schedule.txt
git add schedule.txt && git commit -m "納期を7月末に"
# main 側は、同じ行を「6月末」に変える
git switch main
printf '納期: 6月末\n' > schedule.txt
git add schedule.txt && git commit -m "納期を6月末に"
# 合流しようとすると、同じ行がぶつかってコンフリクトになる
git merge feature
# → CONFLICT (content): Merge conflict in schedule.txt

git merge feature を実行すると CONFLICT (content): Merge conflict in schedule.txt と表示され、schedule.txt の中身は次のようになります (cat schedule.txt で確認できます)。

<<<<<<< HEAD(今のブランチ側 = main)
納期: 6月末
=======
納期: 7月末
>>>>>>> feature(取り込む側)

======= を境にして、上が main 側、下が feature 側の変更です。あとは記号の入った部分を正しい最終形に手で書き直し、3 本の記号 (<<<<<<<=======>>>>>>>) も消してから、addcommit で確定します。

printf '納期: 6月末\n' > schedule.txt # 採用する最終形に書き直す(記号も消す)
git add schedule.txt
git commit -m "コンフリクトを解決(6月末を採用)"

AI に頼むなら

schedule.txt の衝突は『6月末』を採用して、解決を確定して

採用する中身さえ伝えれば、AI はファイルから競合の記号を消し、git addgit commit まで進めて合流を仕上げます。どちらを採るかは私が決め、手作業は AI に任せる。役割分担がはっきりします。

これで合流が完了します。途中で迷ったら git status を打てば、「どのファイルがぶつかっているか」「次に何をすればいいか」を教えてくれます。コンフリクトはエラーではなく、「ここは人間が決めてください」という安全装置だと捉えると、身構えずにすみます。

switch と checkout

なお、ここで使った git switch は、ブランチの作成と移動のための新しいコマンドです (Git 2.23 以降で使えます)。古い記事やチュートリアルでは、同じことを git checkout でやっていることが多いです。git checkout -b feature(作って移動) と git checkout main(移動) が、それぞれ git switch -c featuregit switch main にあたります。

なぜ別のコマンドができたかというと、git checkout は「ブランチの移動」も「ファイルを過去の状態に戻す」も 1 つに詰め込んでいて紛らわしかったからです。そこで、ブランチの移動は switch、ファイルの復元は restore に役割が分けられました。checkout は今でも使えますが、これから覚えるなら、役割がはっきりしている switch がおすすめです。

手元と GitHub をつなぐ

ここまでは手元だけの話でした。チームで使ったり、別のパソコンと共有したりするときに、GitHub のような共有の置き場とやり取りします。この、手元 (ローカル) に対してネットの向こうにある共有の置き場を リモート と呼びます。

気持ちとしては「みんなが集まる置き場」と捉えておけば十分です。git clone したときに、このリモートが自動で origin という名前で登録されます。

リモートとのやり取りでよく使うのは 4 つです。

git clone <URL> # 置き場から、全履歴を手元に複製する
git fetch # 置き場の最新を「取得だけ」する(手元の作業には触れない)
git pull # fetch + merge(取得して、手元に合流まで一気に)
git push # 手元のコミットを置き場へ送る

fetchpull の違いは「合流するかどうか」です。fetch は情報を持ってくるだけなので安全。pull はそのまま手元に取り込むので、コンフリクトが起きることもあります。慎重に進めたいときは、fetch で取得して中身を確認してから merge する、という二段構えが安全です。

実際に、ここまで git-lab で作ってきたコミットを GitHub に上げてみます。第3回gh repo create を済ませてあれば、送り先 (origin) は登録済みなので、git push だけで送れます。

git switch main
git push # 手元の main のコミットを origin へ送る

AI に頼むなら

いまの手元のコミットを共有の置き場(origin)に送って

これで git push が走ります。初めて送るブランチで「上流が未設定」と言われたときも、AI はそのまま git push -u origin <ブランチ名> に切り替えて送り直してくれます。

GitHub のページを開くと、さっき解決したコンフリクトのコミットまで反映されています。「手元でコミット→ GitHub へ push」という往復が、これで一周しました。

なお、チームでは push でいきなり main を変えず、プルリクエスト(変更をレビューしてから取り込む依頼) という作法を使います。これは運用の話なので、第6回でまとめて扱います。

AI に頼むときの言い方が変わる

3 つの領域とマージが分かると、同じ作業でも頼み方が変わります。「いい感じにコミットして」と丸投げするより、領域を踏まえたひと言のほうが、結果がまっすぐ返ってきます。ここまで各コマンドの下に置いたひと言も、裏返せばこの「丸投げとの差」で効いています。

✗曖昧な頼み方◯的確な頼み方裏で動く操作
「いい感じにコミットして」「完成した認証まわりだけステージングして、それでコミットして」git add <該当ファイル>git commit(カゴを選んで使う)
「新機能つくっといて」「main を汚さずに、新機能は別のブランチで試して」git switch -c feature(枝分かれを使う)
「これ保存しといて」「リモートにはまだ送らず、手元でコミットだけしておいて」git commit まで (push しない=リポジトリで止める)

どれも、自分でコマンドを打つわけではありません。けれど「ステージング」「ブランチ」「手元のコミット」という領域の言葉で頼めると、AI は迷わず動けます。曖昧な一言で試行錯誤させるより、結果がまっすぐ返ってきます。

まとめ

第 4 回として、毎日使うコマンドを「鎖のどこを動かすか」で読み解きました。

  • ファイルは 作業ツリー→ステージング→リポジトリ の 3 つの領域を通る。カゴ (ステージング) があるから「何を 1 つの記録にするか」を選べる。
  • git diff は常に「3 つの領域のうち 2 つの比較」。--stagedHEAD で比較相手が変わる。
  • マージには 早送り(しおりをスライド) と 3 方向マージ(親が 2 つの新コミット) がある。コンフリクトは人間に判断を求める安全装置。
  • clone / fetch / pull / pushリモート(GitHub 等の共有の置き場) とやり取りする。fetchpull の差は「合流するか」。

次回は、こわがられがちな rebasereset、そして「squash でまとめる/ブランチを消す・消さない」というマージ戦略を扱います。どれも「鎖のどこを、どこまで動かすか」と「コミットを作り直すとハッシュが変わる」の 2 点で、すっきり理解できます。