プログラマのための IT 教科書
第1部 開発者の道具箱

Git

読了目安 50

この章を読むとできるようになること
  • ブランチが何であるかを説明できる
  • コンフリクトを自分で解消できる
  • reset --hard してしまった作業を reflog で復旧できる

Git は、あなたが毎日使う道具です。そして新人が最も恐れる道具でもあります。

「変な操作をしたら過去の作業が消えるのでは」という不安は、正しい直感です。 実際、git reset --hard は作業を消します。ただしほぼすべての操作は元に戻せます。 この章のゴールは、コマンドを暗記することではなく、 Git が何を保存しているのかを理解して、怖がらずに操作できるようになることです。

Git は何を解決したのか

Git がなかった時代、ファイルはこう管理されていました。

api_v1.zip
api_v2.zip
api_v2_修正版.zip
api_v2_修正版_最終.zip
api_v2_修正版_最終_これ使って.zip   ← どれが最新?

Git が解決するのは3つです。

  1. いつ・誰が・なぜ変えたかが分かる(履歴)
  2. 複数人が同じファイルを同時に触れる(ブランチとマージ)
  3. いつでも過去に戻れる(コミット)

3つの場所

ここが Git 最大のつまずきポイントです。Git にはファイルの状態を置く場所が3つあります。

[作業ツリー]  →  [ステージ]  →  [コミット]
 手元で編集中     git add で載せる   git commit で確定
   した状態         (選ぶ場所)      (履歴に残る)

なぜ2段階なのかというと、コミットする内容を選べるからです。

5つのファイルを直したけれど、そのうち3つだけを「バグ修正」としてコミットしたい。 残り2つは別の作業なので後で別のコミットにしたい。こういう時にステージが役立ちます。

git status は、この3つの場所のを表示するコマンドです。

演習

README.md に開発環境の説明を追記しました。この変更をステージに載せて、「READMEに開発環境の手順を追加」というメッセージでコミットしてください。

コマンドを入力して Enter を押してください。

コミットグラフ
b4fbd88最初のコミットHEAD → main
~/work/shop%
  • 未達「READMEに開発環境の手順を追加」というコミットがある
  • 未達未コミットの変更が残っていない
help で使えるコマンドが見られます
git status を癖にする

Git で迷子になったら git status です。 今どのブランチにいて、何がステージされていて、何がされていないかを教えてくれます。

しかも次に何をすればいいかまで書いてくれます。 エラーメッセージと同じく、読めば答えが書いてあります。

コミットの正体

コミットは「差分」ではありません。その時点の全ファイルのスナップショットです。 そして親コミットへのポインタを持っています。

[最初のコミット] ← [READMEを追加] ← [検索機能を追加]
                                          ↑
                                        HEAD
  • 矢印は「親」を指しています。つまり履歴は後ろ向きに繋がった鎖です
  • HEAD は「今どこを見ているか」を示す印です

この構造が分かると、あとの操作がすべて「ポインタをどう動かすか」の話になります。

コミットメッセージには WHY を書く

悪い: 修正
悪い: README.md を変更
良い: READMEに開発環境の手順を追加(新メンバーが環境構築で詰まったため)

何を変えたかは差分を見れば分かります。 分からないのは「なぜ変えたか」です。 3ヶ月後にこのコミットを見る人(たいてい自分)のために書いてください。

ブランチはポインタでしかない

「ブランチを切る」と聞くと、リポジトリ全体がコピーされる重い操作を想像しがちです。 実際はコミットを指す名札を1つ増やすだけです。だから一瞬で終わります。

main ──────→ [c1] ← [c2]
                      ↑
                add-search(同じコミットを指している)

この状態から add-search でコミットすると、動くのは add-search だけです。

main ──────→ [c1] ← [c2] ← [c3]
                      ↑        ↑
                     main   add-search
演習

add-search という新しいブランチを作ってそちらに移り、search.md というファイルを作って「検索機能の設計メモを追加」というメッセージでコミットしてください。

コマンドを入力して Enter を押してください。

コミットグラフ
b4b9e5fREADMEに開発環境の手順を追加HEAD → mainb4fbd88最初のコミット
~/work/shop%
  • 未達add-search ブランチにいる
  • 未達ブランチ add-search の先頭が「検索機能の設計メモを追加」になっている
  • 達成ブランチ main の先頭が「READMEに開発環境の手順を追加」になっている
help で使えるコマンドが見られます

演習後にグラフを見てください。main は動いていないはずです。 ブランチはポインタ、というのはこういう意味です。

switch と checkout

歴史的な事情で、ブランチを切り替えるコマンドが2つあります。

コマンド用途
git switch ブランチ名ブランチを切り替える(新しく、こちらが推奨
git switch -c 新ブランチ名作って切り替える
git checkout ブランチ名同じことができる(古い。ファイルの復元など他の機能も持つ)

checkout は機能を詰め込みすぎて分かりにくかったため、 ブランチ操作は switch、ファイル復元は restore に分割されました。 新しく書くなら switch を使ってください。 ただし既存のドキュメントや先輩の口からは checkout が出てくるので、両方知っておく必要があります。

マージ

ブランチで作った変更を、main に取り込む操作です。2つのパターンがあります。

fast-forward

main が動いていない場合、ポインタを進めるだけで済みます。

before:  main → [c2],  feature → [c4]
after:   main → [c4](ただ進めるだけ。新しいコミットは作られない)

3-way マージ

両方が進んでいる場合、合流するコミット(マージコミット)が作られます。

        ┌── [c3] ──┐
[c1] ←──┤          ├── [c5] マージコミット(親が2つ)
        └── [c4] ──┘

Git は共通の祖先(c1)を基準に、「どちらが変えたか」を見て自動で統合します。

  • main だけが変えたファイル → main の内容を使う
  • feature だけが変えたファイル → feature の内容を使う
  • 両方が同じファイルの同じ場所を変えたコンフリクト

コンフリクトは壊れていない

コンフリクトが起きると、新人は「壊した」と思って固まります。違います。

Git が「自動では決められないので、あなたが決めてください」と言っているだけです。

ファイルを開くと、こうなっています。

<<<<<<< HEAD
timeout: 60
=======
timeout: 10
retry: 3
>>>>>>> tune-timeout
部分意味
<<<<<<< HEAD から ======= まで今いるブランチ(自分側)の内容
======= から >>>>>>> まで取り込もうとしているブランチの内容

やることは3つです。

  1. マーカーごと消して、正しい内容だけを残す
  2. git add ファイル名 で「解決した」と伝える
  3. git commit でマージを完了する
マーカーを消し忘れる

<<<<<<<======= を消し忘れてコミットすると、 コードにその文字列が残ったまま本番に出ます。構文エラーで気づければ幸運です。

解決したら、必ず git diffgrep -r '<<<<<<<' . で確認してください。

演習

tune-timeout ブランチを main に取り込んでください。コンフリクトが起きるので、timeout は 30、retry は 3 になるように config.yml を直して、マージを完了させてください。

コマンドを入力して Enter を押してください。

コミットグラフ
47e143fタイムアウトを60秒に延長HEAD → main19f8273タイムアウトを10秒に短縮しリトライを3回にtune-timeoute33e476設定ファイルを追加
~/work/shop%
  • 未達config.yml の内容が指定どおりになっている
  • 達成コンフリクトが解消されている
  • 未達「タイムアウト設定をマージ」というコミットがある
help で使えるコマンドが見られます
どちらを採用すべきか分からない時

コンフリクトした相手のコミットを書いた人に聞いてください。 git log でその変更を入れた人と理由が分かります。

勝手にどちらかを消すと、相手の意図した修正が消えます。 これは技術の問題ではなくコミュニケーションの問題です。

rebase と、やってはいけないこと

rebase は、コミットの土台を差し替えて履歴を書き換える操作です。

before:  [c1] ← [c2] ← [c3](main)
              └ [c4] ← [c5](feature)

git switch feature && git rebase main

after:   [c1] ← [c2] ← [c3](main) ← [c4'] ← [c5'](feature)

c4 c5新しい別のコミットc4' c5')として作り直されます。 履歴が一直線になるので読みやすくなります。

push 済みのブランチを rebase してはいけない

これが Git の禁止事項の中で最も重要です。

rebase はコミットを作り直すので、ID が変わります。 すでに push して他の人が取り込んでいるブランチでこれをやると、 相手の手元の履歴と一致しなくなり、全員が混乱します。

自分しか触っていないブランチなら安全、と覚えてください。 チームのルールで「main への rebase 禁止」などが決まっていることも多いので、 最初に確認してください。

この教科書のターミナルでは rebase は動きません(実機で試してください)。

やらかしからの復旧

ここが一番実用的な節です。Git のほとんどの事故は復旧できます。

やってしまったこと復旧方法
ステージに載せたのを取り消したいgit restore --staged ファイル名
作業ツリーの変更を捨てたいgit restore ファイル名
直前のコミットメッセージを直したいgit commit --amend -m "新しいメッセージ"
コミットを取り消したい(履歴を残す)git revert コミットID
コミットを取り消したい(履歴も消す)git reset --hard HEAD~1
reset --hard で消してしまったgit reflog から戻す
作業中だけど別ブランチに移りたいgit stash → 移動 → git stash pop

revert と reset の違い

履歴使う場面
revert打ち消すコミットを新しく作るすでに push した・共有済みのもの
resetコミットそのものを無かったことにする手元だけの、まだ push していないもの

共有済みのものは revert です。reset すると他の人の履歴と食い違います。

reflog は命綱

git reflog は、HEAD が動いた履歴を全部持っています。

$ git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: commit: 大事な実装を追加      ← 消したはずのコミット
c3d4e5f HEAD@{2}: commit: READMEに手順を追加

reset --hard で消えたように見えたコミットも、まだ存在しています (Git はしばらく消しません)。ID が分かれば戻れます。

演習

まず git reset --hard HEAD~1 を実行して、直前のコミット(大事な実装を追加)を消してしまってください。そのあと reflog を使って元に戻してください。

コマンドを入力して Enter を押してください。

コミットグラフ
5fb3838大事な実装を追加HEAD → mainb4b9e5fREADMEに開発環境の手順を追加b4fbd88最初のコミット
~/work/shop%
  • 未達reset --hard で消してしまった
  • 未達reflog で履歴を確認した
  • 達成payment.md が存在する
  • 達成ブランチ main の先頭が「大事な実装を追加」になっている
help で使えるコマンドが見られます
「消えた」と思ったら、まず reflog

新人が最もパニックになるのが reset --hard の事故です。 覚えておくのはこの1行だけでいいです。

git reflog を見れば、たいてい戻せる。

PR ベースの流れ

実務では、main に直接コミットしません。この流れになります。

1. main を最新にする        git switch main && git pull
2. 作業ブランチを作る       git switch -c fix-search-timeout
3. 実装してコミット         git add . && git commit -m "..."
4. リモートに push          git push -u origin fix-search-timeout
5. GitHub で PR を作る
6. レビューを受けて直す     (コミットを追加して push)
7. 承認されたらマージ
8. ブランチを消す           git switch main && git pull && git branch -d fix-search-timeout

ブランチ名

チームの慣習に従いますが、よくある形はこれです。

feature/商品検索の説明文対応
fix/決済のタイムアウト
PROJ-123-add-search    ← チケット番号を入れる方式

何をするブランチか分かる名前にしてください。test tmp mybranch は避けます。

main で作業してしまった

ブランチを切るのを忘れて main でコミットしてしまった場合、慌てなくて大丈夫です。

git branch fix-search        # 今の位置に新しいブランチを作る
git reset --hard origin/main # main を push 済みの位置に戻す
git switch fix-search        # 作ったブランチに移動(作業はここに残っている)

コミットは消えず、ブランチの名札だけが付け替わります。

実務の落とし穴まとめ

  1. 秘密情報をコミットした — 履歴に残るので、削除しても消えません。 気づいた時点ですぐ報告してください(鍵の無効化が必要です)
  2. git add . で余計なファイルを入れるgit status で確認してから add する
  3. コンフリクトのマーカーを消し忘れる — 解決後に git diff で確認
  4. push 済みブランチを rebase する — 全員の履歴が食い違う
  5. コミットメッセージが「修正」 — 3ヶ月後の自分が困る
  6. 巨大な1コミット — 意味のある単位で分ける。レビューしやすさに直結する
秘密情報をコミットしてしまったら

API キーやパスワードをコミットして push してしまった場合、 ファイルを消すコミットを追加しても解決しません。履歴から誰でも取り出せます。

やることは2つです。

  1. その鍵を無効化する(最優先) — これが本当の対処です
  2. チームに報告する

履歴から完全に消す方法もありますが、共有リポジトリでは影響が大きいので、 必ず先輩に相談してください。隠さないことが一番大事です。

まとめ

  • Git には作業ツリー・ステージ・コミットの3層がある。git status はその差を見せる
  • コミットはスナップショット + 親へのポインタ。履歴は後ろ向きの鎖
  • ブランチはポインタでしかない。だから作るのが一瞬で、怖くない
  • コンフリクトは壊れていない。マーカーを消して add して commit すれば終わる
  • 共有済みは revert、手元だけなら reset
  • reset --hard しても git reflog で戻せる
  • push 済みブランチを rebase しない
  • 秘密情報をコミットしたら、鍵を無効化して報告

章末問題

git switch -c feature でブランチを作ってコミットしました。main はどうなっていますか。

すでに push してチームが取り込んでいるコミットに、バグがあると分かりました。取り消す方法は?

git reset --hard HEAD~1 を実行してしまい、3時間かけた作業が消えました。最初にやることは?

次の章からは第2部です。チームでこの Git をどう使って開発を回すのか、 という仕事の流れの話に移ります。

読み終わったら記録しておくと、目次で進み具合が分かります。