第3章 バージョン管理と変更レビュー

学習目標

  • ワーキングツリー、ステージングエリア、コミット履歴、リモートリポジトリの関係を説明する。
  • 比較する対象を選んでdiffを読み、エージェントが行った変更の中から、範囲外の修正とチェックを弱めた変更を見つける。
  • 1つの目的だけを含むコミットを作り、同じファイル内の変更も選んでコミットする。
  • 共有の有無に応じて、restore、reset、revert、stashから適切な取り消し方法を選ぶ。
  • 消えたコミットをreflogで復旧し、両方の変更の意図を生かして競合を解決する。

はじめに

コーディングエージェントは、数分で何十ものファイルを修正します。人が1日かける作業を代わりに行ってくれますが、その分、何が変わったかを見落としやすくもなります。エージェントの報告は「日付フィルターを追加しました」の1行でも、実際には日付の計算関数が丸ごと書き直され、新しいパッケージが1つ導入され、テストが1つ、知らない間に無効になっているかもしれません。

この違いを示すのがGitです。Gitは何が変わったかを行単位で示し、気に入った状態を記録して、いつでも戻れるようにします。エージェントと働くとき、Gitは単なる選択肢ではなく、欠かせないセーフティネットです。大胆な試みを任せられるのも、いつでも戻れる地点があるからです。

この章では、Gitの基本構造から始め、diffの読み方とレビューの方法、コミットとブランチの使い方、競合の解決方法、状況に合った取り消し方法を順番に扱います。「さらに深く」では、消えたコミットを復旧する方法、共有済みの履歴を書き換える方法、複数のエージェントを同時に動かす方法を見ます。

重要な概念

Gitはセーブポイント付きのタイムマシン

Gitは、ファイルの変更履歴を残すバージョン管理ツールです。ゲームのセーブポイントのように、気に入った状態になるたびに記録しておけば、いつでもその状態に戻ることができます。このセーブポイントをコミット(commit)と呼び、コミットごとに、なぜ変更したかを書くメッセージが付きます。GitHubのドキュメントは、この記録を時点ごとのスナップショットと呼んでいます。

Gitでの変更は、3つの領域を通ります。

領域 例え 説明
ワーキングツリー 作業台 現在ファイルを修正している場所
ステージングエリア(インデックス) 梱包する箱 次のコミットに入れると選んだ変更を集める場所
コミット履歴 倉庫の棚 確定したセーブポイントが積み重なる場所

HEADは、現在チェックアウトしているブランチの最新コミットを指す印で、「今立っているセーブポイント」です。現在の状態はgit statusで確認します。git status --shortのMは、追跡中のファイルが修正されたことを、??はGitがまだ追跡していない新しいファイルであることを意味します。エージェントに作業を任せる前に、この出力がクリーンかを確認する習慣をつけてください。Codexのドキュメントも、作業を任せる前に機能ブランチで作業してgit statusをクリーンに保ち、頻繁にコミットして、元に戻しやすくするよう勧めています。

自分のコンピューターの履歴とリモートリポジトリ

GitHubによるとGitは分散型で、開発者それぞれがプロジェクトと全履歴のコピーを持ちます。自分のコンピューターのリポジトリをローカル、GitHubなど共用サーバー上のものをリモート(remote)と呼びます。

コマンド 役割
git clone 初めてリモートリポジトリ全体を取得する
git fetch リモートの新しい履歴を取得し、自分の作業には混ぜない
git pull 取得した履歴を自分のブランチに統合する
git push ローカルコミットをリモートに送る

この区別が大切なのは、取り消す方法が共有の有無で変わるからです。未プッシュのコミットは自分だけの履歴なので、消したり書き換えたりできます。プッシュして他人が受け取ったコミットを消すと、仲間の履歴と食い違います。

diff:何と何を比較するか

diffは2つの状態の違いを行単位で示します。エージェントの作業を最も正確に伝えるのは、報告ではなくdiffです。ただし同じgit diffでも、何と何を比較するかによって見えるものが異なります。

git diff                 # ワーキングツリーとステージングエリア:未ステージの変更
git diff --staged        # ステージングエリアと最後のコミット:次のコミットに入る変更
git diff HEAD            # ワーキングツリーと最後のコミット:コミット以降の全変更
git diff main...feature  # 2つのブランチの分岐点からfeatureで変わった内容

--stagedは--cachedと同じ意味です。大きな変更は要約から見ます。--statはファイルごとに何行変わったかを、--name-onlyは変更されたファイルの名前だけを表示します。エージェントがインデントや行末の空白を大量に変更し、本来の変更が埋もれた場合は、-wで空白の違いを無視します。1行の中の小さな変化は、--word-diffで単語ごとに見ます。

新しく作ったファイルはGitがまだ追跡していないため、git diffに出ません。エージェントが新しいファイルを作ったかはgit statusの??で確認し、ステージングしたあと、git diff --stagedで内容を見ます。

diffをレビューする順番とチェックリスト

すべての行を同じ深さで読むと疲れます。まず範囲を見て、危険な場所から詳しく読みます。

  1. 範囲を見る。 git diff --statでファイル数と場所を確認します。依頼した内容に比べてファイル数が多ければ、まずその点を疑います。
  2. 危険な場所を先に開く。 認証、決済、権限、設定ファイル、データベースの構造、依存関係の一覧(package.jsonとロックファイル)が変更されていれば、その理由が明確かを確認します。
  3. 消えたものを探す。 削除されたテスト、消されたエラー処理、抜け落ちたチェックの段階を探します。
  4. 1つの値を見る。 並び順がa.date - b.dateからb.date - a.dateに逆転する例のように、1行の中の小さな変化が動作を大きく変えます。
  5. 入ってはいけないものを探す。 シークレット、ビルドの生成物、一時ファイル、ログが紛れ込んでいないかを確認します。こうしたファイルは、.gitignore(第4章)で初めから追跡しないようにします。
  6. 理解できない変更は尋ねる。 「なぜこのファイルを変更したのですか」という問いに答えられない変更は、受け入れません。

チェックを弱めた変更

エージェントは、失敗をなくすように頼まれると、アプリを修正する代わりにチェックを弱めてしまうことがあります。火災報知器が鳴ったからといって、報知器の電池を抜くのと同じです。diffでは、次の兆候を探します。

兆候 内容
テストの削除 テストファイルやテストのブロックが丸ごと消える
スキップ it.skip、describe.skip、xitなどが加わる
期待値の緩和 具体的な値の比較が「存在する」程度になる、期待値が実際の値に変わる
無効化コメント 型チェックやリントを無効にするコメントが加わる
設定の緩和 厳格な型チェックをオフにしたり、対象からフォルダを外したりする
自動化の段階の削除 CIの設定から、テストやリントを実行する段階が抜ける

要件が変わったためにテストも変更しなければならない場合があるので、判断の基準は「その変更を正当化する要件があるか」です。なければ、元に戻します。

コミットは小さく、目的は1つ

GitHub flowの案内は、コミットごとに何が変わったかわかる説明を付け、理想的には1つのコミットに、独立して完結した1つの変更を含めるべきだと述べています。そうすれば、後でその変更だけを選んで元に戻しやすくなります。「文言の修正」と「認証ロジックの変更」が1つのコミットに混ざると、認証だけを元に戻そうとしても、必要な部分を手で選び出し、もう一度検証しなければなりません。書式の整理(インデント、引用符)は、特に機能の変更と混ぜないようにします。書式の変更が数百行もあると、本来の変更がその中に埋もれてしまいます。

エージェントが1つのファイルの中に必要な修正と不要な修正を一緒に残した場合は、git add -pで選んでステージングします。Gitは変更をハンク(hunk、連続している変更のまとまり)単位で表示し、1つずつ尋ねます。

キー 意味
y このハンクをステージングする
n このハンクはステージングしない
s ハンクをさらに小さく分ける
e エディターで直接編集する
q 終了する。このハンクと残りはステージングしない

メッセージには何を変えたかと、なぜ変えたかを書きます。「バグ修正」より「午前の予約がフォーマット検証で拒否される問題を修正」のほうが、3か月後にずっと役立ちます。

ブランチとプルリクエストの流れ

ブランチ(branch)は、本来の流れ(通常はmain)から分かれた、別の作業の流れです。本編とは別に行う試し撮りのようなものです。GitHub flowの案内が勧める順番は次のとおりです。

  1. 短く内容を表す名前でブランチを作る。無関係な変更は別のブランチにする。
  2. ブランチ上で、説明を付けたコミットを積み重ねる。
  3. プルリクエスト(pull request、PR)を開き、変更の要約を書いてレビューを受ける。指摘には同じブランチへの追加コミットで応える。
  4. 承認後にマージし、ブランチを削除する。PRとコミット履歴は残る。

エージェントに大きな作業を任せるとき、ブランチは特に役立ちます。結果が気に入らなければ、mainはそのままにしてブランチごと捨てることができます。PRごとにプレビューデプロイが作られるなら(第5章)、マージする前に実際の画面も確認します。

競合は意味を考えて解決する

2つのブランチが同じファイルの同じ行をそれぞれ異なる内容に変更すると、Gitはどちらを選ぶべきかわからないため、停止します。GitHubのドキュメントは、同じ行を異なる内容に変更した場合や、一方がファイルを修正し、もう一方が削除した場合に競合が起き、すべての競合を解決して初めてマージできると説明しています。競合したファイルには印が残ります。

<<<<<<< HEAD
const MAX_GUESTS = 8;
=======
const MAX_GUESTS = 6;
const WAITLIST_ENABLED = true;
>>>>>>> feature/waitlist

=======の上が現在のブランチ(HEAD)、下が取り込む側です。解決とは印を消すことではなく、両方の変更の意図を生かす結果を決めることです。この例では、「定員を8人に増やす決定」と「キャンセル待ちを有効にする決定」の理由を確認し、両方が生きるコードを書きます。一方を丸ごと採用すると、もう一方の決定が静かに消えます。

エージェントに競合の解決を任せるときは、両方の変更がそれぞれどんな目的だったかを伝え、解決後にテストを再実行させます。途中で判断できなくなったら、git merge --abortでマージ前に戻れます。Gitのドキュメントは、このコマンドで未コミットの変更を完全には復元できない場合があるため、マージ前にコミットかstashをするよう勧めています。

取り消し方法の判断表

取り消す対象と、履歴の扱いはコマンドごとに違います。

コマンド 対象 使う状況 注意
git restore <ファイル> 未コミットのワーキングツリーの変更 直前の修正を捨てる 既定ではステージングエリアの内容に戻す。未ステージの修正を捨てるとGitで復旧できない
git restore --staged <ファイル> ステージングエリア ステージングだけを取り消す ワーキングツリーの修正は残る
git stash push -u 未コミットの変更と未追跡の新規ファイル(無視するファイルは--allの場合のみ) 捨てたくない変更を一時的に片付ける applyは一覧に残して適用、popは適用成功時に一覧から削除
git reset --soft <地点> コミット履歴のみ 未共有の複数コミットを1つにまとめる 戻したコミットの変更はステージングエリアに残る
git reset --hard <地点> 履歴、ステージング、ワーキングツリー全体 未共有のローカルコミットを丸ごと捨てる 未コミットの変更も上書きして破棄する
git revert <コミット> 記録済みのコミット 共有済みコミットの変更内容を打ち消す 履歴を残したまま、変更を打ち消す内容を新しいコミットとして記録する。ワーキングツリーがクリーンであること

resetには--keepもあります。コミットを捨てつつ未コミットの修正を残し、現在のHEADと移動先のコミットとの間に差分があるファイルに未コミットの修正があれば、処理を中断します。Gitは、他人に渡したコミットに--hardを使わないよう明記しています。

要点

判断は簡単です。未コミットならrestoreかstash、コミット済みで未共有ならreset、プッシュして他人が受け取ったならrevertです。

マージコミット(ブランチを統合したコミット)は親が複数あるため、どちらを基準に戻すか-mで指定します(通常-m 1)。Gitのドキュメントは、マージコミットを打ち消すと、そのマージで導入された変更を今後も適用しないと宣言した扱いになるため、後で同じブランチを再マージしても、その変更は自動では再導入されないと警告しています。

エージェントの巻き戻し機能とGit

Claude Codeは、新しいターンを始める要求を送るたびにチェックポイントを作り、ファイルの状態を保存します。入力欄が空のときにEscを2回押すか、/rewindでメニューを開けば、コードだけ、会話だけ、両方を以前の地点に戻せます。便利ですが、公式に明示された限界があります。

  • ファイル編集ツールによる変更だけを追跡する。rm、mv、cpなどシェルコマンドによる変更は戻せない。
  • 人が直接編集したファイルや、同時実行の別セッションの編集は通常含まれない。
  • サブエージェントが編集したファイルは、たいてい復元されない。
  • スナップショットは既定で約30日後に整理される。
  • バージョン管理の代わりにならない。永続的な記録と協業にはGitを使い続ける。

巻き戻しは、セッション内ですぐミスを取り消す補助手段です。本当の安全網は検証済みの地点ごとのコミットです。Codexは、作業フォルダへの書き込みを認めるモードでも.gitを読み取り専用で保護し、履歴自体を壊せないようにします。

さらに深く

消えたコミットの復旧:reflog

git reset --hardを誤って実行し、git logから消えたコミットも、たいてい復旧できます。ブランチの先端が動くたびに、Gitがreflog(参照履歴)に残すからです。消えたと思ったコミットも、しばらくはリポジトリ内に残っています。

git reflog                      # HEADが経由した位置の一覧
# a1c9e02 HEAD@{0}: reset: moving to HEAD~2
# 7f3b8d1 HEAD@{1}: commit: 定期決済の案内文を追加
git branch rescue 7f3b8d1        # 消えたコミットにブランチを付けて復旧

HEAD@{2}は「2回前にHEADがいた場所」です。直前のresetを取り消すなら、git reset --hard ORIG_HEADも使えます(reset直前の位置がORIG_HEADに残ります。mergeやrebaseなどの別のコマンドで上書きされるため、resetの直後に実行します)。ただしreflogは永久ではありません。Gitによると、既定の保存期間は現在のブランチから到達可能な項目が90日、到達不能な項目が30日です。またローカルの記録なので、一度もコミットしていない変更は含みません。一度もステージングしていない修正を--hardで上書きすると、Gitでは復旧できません。ステージングした内容はGitオブジェクトに残り、git fsck --lost-foundで探せる余地がありますが、頼らないほうがよいでしょう。

共有済みの履歴を書き換えるとき

プッシュする前なら、乱雑なコミットを整理しても構いません。Gitのドキュメントの例のように、git reset --soft HEAD~5のあとでgit commitを実行すると、最後の5つのコミットが1つにまとまります。問題になるのは、すでにプッシュした履歴を書き換えるときです。

書き換えた履歴をリモートに送るには、強制プッシュが必要です。git push --forceは、リモートの状態を確かめずに上書きします。自分が履歴を修正している間に、仲間が同じブランチに新しいコミットを送っていたら、そのコミットは消えてしまいます。git push --force-with-leaseは、リモートのブランチが、自分が最後に取得した地点にそのままとどまっている場合だけ上書きし、その間に動いていれば拒否されます。ただし、Gitのドキュメントは、バックグラウンドで自動的にgit fetchが実行されると、「最後に見た状態」が知らない間に更新され、この保護が効かなくなる可能性があると警告し、--force-if-includesを併用する方法を紹介しています。

GitHubは、保護ブランチへの強制プッシュを既定で阻止します。共有ブランチの履歴を書き換えなければならない状況自体を減らすことが、最善です。機能ブランチを維持する期間を短くし、履歴の整理はプッシュする前に行います。

機能ブランチを作らず、ローカルのmainで直接コミットしてしまう間違いもよくあります。リモートへ送る前なら、まず現在の位置にブランチ名を付けてコミットを確保し(git branch <新しいブランチ>)、mainをリモートと同じ地点に戻したあと(git reset --hard origin/main)、新しいブランチに移って作業を続ければよいのです。順番が大切です。先にブランチを作らず、resetから実行すると、コミットはreflogにだけ残ります。

複数のエージェントを同時に動かすとき

複数セッションが1つのフォルダで同時に働くと、同じファイルを交互に直し、1つのコミットに別々の作業が混ざりがちです。1つの厨房で2人が同じまな板を使うようなものです。解決策はワークツリー(worktree)です。Gitによると、1つのリポジトリに複数の作業ツリーを接続し、異なるブランチを同時にチェックアウトできます。

git worktree add -b feature/coupon ../shop-coupon   # 新しいブランチを新しいフォルダに
git worktree list
git worktree remove ../shop-coupon                  # 終わったら片付ける

Claude Codeは、claude --worktree <名前>で、リポジトリの.claude/worktrees/<名前>/の下に新しいブランチのワークツリーを作り、その中でセッションを開始します。セッションごとにファイルのコピーが別々にあるので、編集が衝突しません。ワークツリーは新しくチェックアウトしたコピーで、.envなどの無視されたファイルがありません。そのため、Claude Codeのドキュメントは、このツール専用の設定ファイルである.worktreeincludeで必要なファイルをコピーさせ、.claude/worktrees/を.gitignoreに入れるよう案内しています。

同時実行はレビューにも役立ちます。Claude Codeのベストプラクティスでは、新しいコンテキストのほうがコードレビューの質が高くなるとしています。直前に書いたコードに対する先入観を持たずにレビューできるからです。一方のセッションが実装し、別のセッションがレビューする「作成者・レビュアー」方式や、サブエージェントに計画ファイルとdiffだけを渡し、要件の抜けと範囲外の変更を探させる方式があります。ただし、ドキュメントには、穴を探すよう指示されたレビュアーは、作業に問題がなくても何かを報告しがちだという注意もあります。そのため、正確性と要件に影響する指摘だけを受け入れるよう付記しています。

実務の場面

風の道自転車:並べ替え1つで9個のファイルが変わる

シェア自転車の貸出所「風の道自転車」の運営者ソさんは、エージェントに「貸出一覧を返却予定時刻の順に並べてほしい」と任せました。エージェントは完了を報告し、テストも通りました。ソさんは、いつものようにgit diff --statから確認しました。2つのファイルで十分に見える作業なのに、変更されたファイルは9個ありました。

内容を見ると3種類でした。一覧画面と並べ替え関数の修正は依頼どおりでした。5つのファイルは引用符とインデントだけの書式整理でした。さらにpackage.jsonとロックファイルに日付ライブラリが加わり、返却時刻計算のテスト1つにit.skipが付いていました。skipのあるテストは実行されずスキップ扱いになるので、通過の理由の1つがそのスキップだったのです。

ソさんは、3つの選択肢を並べて検討しました。

  • そのまま受け入れる:テストが通っているので、手軽です。しかし、無効になったテストと新しい依存関係を、理由もわからないまま履歴に残すことになります。この案は退けました。
  • すべて捨てて、もう一度任せる:まっさらになりますが、依頼どおりにできている並べ替えの修正まで捨てることになります。この案は退けました。
  • 選んでステージングする:必要な変更だけをコミットし、残りを元に戻します。この方法を選びました。

理由を尋ねると、エージェントは「標準の日付比較ではタイムゾーンの処理が面倒なのでライブラリを導入し、そのために既存のテストと結果が異なったのでスキップした」と答えました。ソさんは-wオプションで書式の変更を除き、並べ替え関数のdiffだけをもう一度見ました。既存の日付比較でも並べ替えは可能でした。ソさんはライブラリを使わずに修正させ、スキップしたテストを元に戻して、全体のテストを実行しました。並べ替え関数があるファイルにも書式の変更が混ざっていたため、git add -pで並べ替えのハンクだけをy、書式のハンクをnとして選びました。残りの書式変更だけのファイルは、git restoreで元に戻しました。

コミットメッセージは「貸出一覧を返却予定時刻順に並べる」として残しました。書式の整理が必要なら、次にその変更だけを含むコミットとして、別に行うことにしました。テストが通ったことだけを見て受け入れていたら、無効になったチェック1つと、誰も把握していない依存関係が、履歴に紛れ込んでいたでしょう。

ひかり合唱団:金曜日のコミット、月曜日の発見

団員の出席管理アプリを運営する「ひかり合唱団」の総務担当テさんは、月曜日の朝に「出席率が不自然に低く表示される」という連絡を受けました。金曜日の午後には正常でした。その間に、mainにはテさんと他のボランティア2人が作ったコミットが11個積み重なり、すべてすでにリモートへプッシュされていました。

どのコミットが原因か、1つずつ開くには多すぎました。出席率を確認するテストがなかったので、テさんはまず、金曜のデータで出席率を計算し、期待値と比較する小さなスクリプトをエージェントに書かせました。期待値と同じなら終了コード0、違えば1を返し、ビルドできず確認不能なコミットでは125を返してスキップするものでした。そしてGitの二分探索を実行しました。

git bisect start HEAD <金曜日の正常なコミット>
git bisect run node scripts/check-attendance.mjs
git bisect reset

git bisectは正常と異常の間の中間コミットを取り出し、スクリプトを実行し、結果に従って範囲を半分に狭めます。4回で原因が見つかりました。欠席理由の追加時に「公欠」を欠席に数える条件へ変えたコミットでした。

ボランティアの1人は、履歴を金曜日の地点に丸ごと戻して、リモートに上書きしようと提案しました。テさんは断りました。すでに2人が受け取った履歴を消すと、その人たちのローカルの履歴と食い違い、原因と関係のない10個のコミットも一緒に消えてしまいます。テさんは、原因のコミット1つだけをgit revertしました。

取り消している途中で競合が起きました。原因となったコミットのあとで、他のボランティアが同じ関数に「遅刻」の処理を追加していたからです。テさんは競合の印を消す前に、両方の変更の意図を整理しました。元に戻すのは「公欠を欠席に数えた条件」で、守るのは「遅刻の処理」でした。両方の意図を生かしたコードを書き、出席率のスクリプトと全体のテストをもう一度実行してからプッシュしました。出席率のスクリプトはそのままテストに移し、同じミスが再び入ったらCIで検出できるようにしました。

間違えやすいポイント

  1. 「テストが通ったので、diffは見なくてもよい。」 テストが通ると安心します。しかし、通過したことは、変更の範囲が正しいことを意味しません。テストが削除されたり、無効にされたりしたために通った可能性もあります。
  2. 「問題のあるコミットはresetで消して強制プッシュすれば、きれいになる。」 履歴はきれいに見えます。しかし、共有済みの履歴を書き換えると、仲間の履歴と食い違い、他の人の作業まで失う可能性があります。共有済みのコミットは、revertで取り消します。
  3. 「コミットは1日の終わりに1回で十分だ。」 回数が少ないと履歴が単純に見えます。しかし大きなコミットから一部だけ戻すのは難しく、選んだ結果の再検証も必要です。検証済みの小さな単位ごとにコミットします。
  4. 「git diffに出なければ、変更はない。」 コマンドが何も表示しないと、クリーンだと感じます。しかし、新しいファイルはまだ追跡されていないので表示されず、ステージングした変更は--stagedで見なければ表示されません。git statusも一緒に確認します。
  5. 「競合は一方を丸ごと採用すれば、早く終わる。」 印は早く消えます。しかし、もう一方の決定も知らない間に消えてしまいます。両方の意図を確認し、どちらも生かしてから、テストをもう一度実行します。
  6. 「エージェントの巻き戻し機能があるので、コミットは少なくてもよい。」 巻き戻しは便利です。しかし、シェルコマンドで変更されたファイルや、別のセッションでの編集は元に戻せず、Gitの代わりにはなりません。

ひと目でわかるまとめ

話題 覚えておくこと
3つの領域 ワーキングツリー → ステージング → コミット。git statusで確認し、??は新規ファイル
共有の有無 プッシュ前は書き換え可、プッシュ後はrevert
diff 通常のgit diffは未ステージ、--stagedは次のコミット、HEADはコミット以降すべて
レビュー順 --stat → 危険なファイル → 消えたもの → 1つの値 → 混ぜてはいけないもの
弱めたチェック 削除、skip、期待値緩和、無効化コメント、設定緩和、CI段階の削除
コミット 目的は1つ、書式と機能は分離、git add -pで選択、理由をメッセージに
競合 印を消すことではなく、両方の意図を生かすこと。判断できなければgit merge --abort
取り消し 未コミットはrestore・stash、未共有はreset、共有済みはrevert。マージは-m
復旧 reflogで位置を探しブランチを付ける。未コミット変更は含まない
同時作業 セッションごとにワークツリー。レビューは新しいコンテキストで

公式ドキュメントで確認する

セルフチェック

3-1. git diff、git diff --staged、git diff HEADは、それぞれ何と何を比較しますか。ステージングした変更としていない変更を、一度にすべて見るにはどれを使うかを示してください。

3-2. エージェントは、src/lib/receipt.tsという新しいファイルを作ったと報告しましたが、git diffには何も表示されません。その理由を説明してください。また、このファイルの内容をdiffで見るにはどうすればよいかを説明してください。

3-3. エージェントが機能を1つ修正する際、10個のファイルのインデントをタブから空白に変更しました。diffが数千行になり、本来の変更を見つけにくくなっています。本来の変更だけを見るにはどのオプションを使うか、またコミットをどう分けるとよいかを説明してください。

3-4. 共有済みのmainで、機能ブランチを統合したマージコミットを戻します。git revertに何を追加指定しますか。また同じブランチを修正して再マージする場合の注意点を説明してください。

3-5. stashに入れた実験中の変更を、別の2ブランチに同様に適用します。git stash applyとgit stash popのどちらを使うか、理由とともに説明してください。

3-6. まだプッシュしていないローカルコミット2つを捨てたいと考えています。しかし、ワーキングツリーには、まだコミットしていない別のファイルの修正があり、それは残したい状況です。--hardの代わりに使えるresetのモードと、どのような場合にそのコマンドが停止するかを説明してください。

3-7. git bisect runに渡す確認用のスクリプトを書いています。あるコミットはビルドすらできず、正常かどうか判断できません。そのようなコミットでは、スクリプトがどの終了コードを返すべきかを示してください。

実習課題

実習

4つの取り消しと復旧(45分)

  1. 新しいフォルダでgit initを使ってリポジトリを作り、ファイルを1つ、3回修正して毎回コミットする。
  2. 4回目の修正はコミットせずにgit stash pushで一時的に片付け、git stash applyで復元したあと、今度はgit restoreで捨てる。
  3. 2番目のコミットをgit revertで取り消し、git log --onelineで新しいコミットが作られたことを確認する。
  4. 新ブランチで1つコミットし、git reset --hard HEAD~1で捨てる。
  5. git reflogでそのコミットを見つけ、git branch rescue <ハッシュ>で復旧する。

完了基準: 各段階でのgit log --onelineの出力が記録され、revertでは履歴が増え、resetでは減るという違いと、復旧したコミットがrescueブランチにあることを、出力で示せる。

実習

エージェントの変更を選んでコミットする(45分)

  1. 練習用アプリでエージェントに小さな機能を任せ、わざと「ついでにコードの書式も整理してほしい」と付け加える。
  2. コミットする前に、git diff --stat、git diff -w、git statusを使って、機能の変更、書式の変更、新しいファイルを区別し、表に記す。
  3. チェックを弱めた変更(削除、skip、期待値の変更)があるかを探し、あれば理由を尋ねる。
  4. git add -pで機能の変更だけを選んで最初のコミットを作り、書式の変更は2番目のコミットとして別に作る。
  5. 両方のコミットでテストが通るかを確認する。

完了基準: 2つのコミットがそれぞれ1つの目的だけを含んでおり(git show --statで確認する)、変更を分類した表と、チェックが弱められていないかを確認した結果が残っている。また、両方のコミットでテストが通過した出力がある。