第9回では、TODO アプリに完了チェックと削除を feature ブランチで足し、コンフリクトを AI と解決しました。今日はまず、ブラウザを閉じてもタスクが消えない保存機能を入れます。そのうえで、開発につきものの「やっぱり戻したい」を実戦で扱います。
戻し方には種類があります。revert、reset、reflog。仕組みは第5回で見たとおりですが、本物のアプリで使うと、判断がついて回ります。いちばん大事なのは「そのコミットは、もう GitHub に push したか」です。push 済みかどうかで、安全な戻し方が変わります。そして第9回のコンフリクトと同じで、この判断も AI に丸投げせず、選択肢を説明させてから自分で決めます。
TL;DR
- 第9回のアプリに、localStorage でタスクを保存する機能を
featureブランチで足します。リロードしてもタスクが残ります。 - push 済みのコミットを取り消すときは、
revert(打ち消すコミットを足す・安全) かreset(履歴を巻き戻す・force が必要で危険) かを、AI に選択肢と適否を説明させてから決めます。 resetの 3 モード (--soft/--mixed/--hard) の違いは、git statusの表示を見れば直感でわかります。--hardで消してもreflogがあるので、約 90 日は救出できます。散らかったコミットはreset --softで 1 つにまとめ、書き換えた履歴は--force-with-leaseで安全に上げ直します。
まず AI に localStorage 保存を作らせる
今のアプリは、リロードするとタスクが消えます。第8回で AI が「保存はしない」と判断した部分です。ここに保存機能を足します。
タスクがページを再読み込みしても消えないようにして。ブラウザのローカルに保存する形で。 新しいブランチで作業して、できたら main に取り込んで、GitHub にも反映して。
AI は feature/localstorage-persist ブランチを切り、保存と復元の処理を足して --no-ff でマージし、GitHub まで反映しました。中心になるのは、この 2 つの処理です。保存する側は、画面のタスクを配列にして localStorage に書き込みます。
// タスクの状態を localStorage に保存する
function saveTodos() { const items = Array.from(list.querySelectorAll('li')).map(function (li) { return { text: li.querySelector('span').textContent, done: li.classList.contains('done') }; }); localStorage.setItem('todos', JSON.stringify(items));
}復元する側は、ページを開いたときに localStorage から読み戻して、画面に並べ直します。
// ページ読み込み時に保存済みタスクを復元
const saved = localStorage.getItem('todos');
if (saved) { JSON.parse(saved).forEach(function (item) { list.appendChild(createListItem(item.text, item.done)); });
}ここでも「指定した値」と「AI が勝手に決めた値」が分かれます。私が伝えたのは「リロードしても消えないように」「ローカルに保存」だけ。次は AI が補いました。
- localStorage のキー名を
todosにした - データの形を
{text, done}の配列にして、完了したかどうかまで保存した (リロードで完了が外れない) - 追加・削除・チェック変更の各操作のたびに、すぐ保存するようにした
完了状態まで保存したのは、地味ですが効く判断です。「リロードしても消えない」とだけ頼んだのに、AI は「完了のチェックも残るべきだろう」と読み取りました。実機で 3 件入れて 1 件完了させ、ページを再読み込みすると、件数も打ち消し線もそのまま残っていました。
取り消したいとき、revert か reset か
ここからが本題です。保存のついでに、キー名を todos から todo-app-data に変える小さな変更を 1 つコミットして、GitHub に push しました。そのうえで「やっぱり元に戻したい」となった、という場面を作ります。
戻し方を AI に丸投げると、勝手に決めます。第9回のコンフリクトと同じ失敗です。なので、こう頼みました。
やっぱり戻したい(履歴は残してよい)。取り消し方の選択肢と、push 済みでの適否を 説明してからやって。
AI は実行する前に、3 つの選択肢を、push 済みでの安全性つきで並べました。
| 方法 | 仕組み | push 済みで安全か |
|---|---|---|
git revert <hash> | 取り消す内容の新しいコミットを足す | 安全 (推奨) |
git reset <hash> | 履歴を巻き戻して、コミットを無かったことにする | 危険 (force が必要・他人の作業を消しうる) |
| 手動で書き戻して新コミット | revert と実質同じ | 安全だが手間 |
そのうえで AI は、revert を選んだ理由をこう説明しました。問題のコミットはすでに push 済みで、GitHub 上で公開されている。reset で履歴を巻き戻すと、その公開済みの歴史が変わってしまい、上げ直すには force(強制上書き) が要る。もし他の人がその歴史をもとに作業していたら、その作業を壊しかねない。だから、履歴を残したまま打ち消す revert が安全だ、と。
この判断の絵を 1 枚にすると、こうなります。
flowchart TD C3["取り消したいコミット<br/>(すでに GitHub に push 済み)"] --> RV["revert:打ち消す新コミットを足す<br/>履歴が残る→ push 済みでも安全"] C3 --> RS["reset:コミットを無かったことにする<br/>履歴が変わる→ force 必須・公開済みは危険"]
説明に納得してから、実行を任せます。
git revert 319183c --no-edit git push
git revert は、319183c(キー名を変えたコミット) の中身を打ち消す新しいコミットを作ります。履歴は消えません。「変えた」記録と「戻した」記録が、両方とも残ります。push 済みでも、新しいコミットを足すだけなので、force は要りません。普通の git push で送れます。
「push 済みかどうかで revert か reset かを決める」。この判断軸さえ持てば、戻し方で迷いません。判断は私がして、手は AI に動かしてもらう。第9回からの分担は、ここでも同じです。
reset 3 モードを git status で見る
reset は公開済みの履歴には危険ですが、まだ push していない手元のコミットになら、安全で便利です。第5回で見た 3 つのモードの違いを、実際に git status の表示で確かめます。
小さなコミットを 2 つ作ってから、「直前のコミットを取り消す」のを 3 パターンで試すよう頼みました。
小さなコミットを2つ作って。そのあと「直前を取り消す」を、(A) 変更は作業場に残す / (B) ステージに残す / (C) 変更ごと完全に消す、の3パターンで実演して。
AI が 3 つを順に実演し、それぞれ直後の git status がどう変わったかが、違いをそのまま物語ります。
| 指示 | コマンド | 直後の git status | 変更の居場所 |
|---|---|---|---|
| (A) 作業場に残す | git reset HEAD~1(=--mixed) | Changes not staged for commit | 作業ツリー |
| (B) ステージに残す | git reset --soft HEAD~1 | Changes to be committed | ステージング |
| (C) 完全に消す | git reset --hard HEAD~1 | nothing to commit, working tree clean | 消えたように見える |
第4回で見た「3 つの領域」(作業ツリー→ステージング→リポジトリ) を思い出すと、すっきりします。reset はコミットを取り消す操作ですが、取り消した変更をどの領域まで戻すかが 3 モードの違いです。--soft はステージングまで、--mixed(既定) は作業ツリーまで戻し、--hard はどこにも残さず捨てます。
(C) の --hard だけは、変更が消えます。ここで初心者は身構えます。でも、次の節で見るように、消えたように見えるだけです。
消したコミットを reflog で救出する
--hard で消したコミットは、本当に取り返せないのか。AI に聞いてみます。
さっき (C) で消したコミット、復活できる? できるなら復活して。
AI は git reflog を使いました。reflog は、第5回で「やらかしてからも数週間は戻せる猶予」として紹介した安全網です。HEAD が動いたすべての記録が残っているので、--hard で消したつもりのコミットも、ここにハッシュが残っています。
git reflog
# …消したはずのコミット 1b2493f が HEAD@{1} などとして残っている
git cherry-pick 1b2493fgit cherry-pick で、そのコミットを今のブランチに改めて取り込めば、復活します。--hard は HEAD というしおりを動かしただけで、コミットの実体 (第3回で見たオブジェクト) は消えていなかったのです。到達できなくなったオブジェクトも、約 90 日は reflog から拾えます。
「--hard でも reflog がある」。これがわかると、reset が怖くなくなります。取り消しを思い切って試せるようになることが、Git に慣れる入口です。
散らかったコミットを squash でまとめる
ここまでの実演で、小さなコミットが何個か散らかりました。これを 1 つにまとめます。
散らかった小コミットを1つにまとめて。
AI が使ったのは、第5回でも触れた最もシンプルなまとめ方です。
git reset --soft HEAD~2 git commit -m "..."
git reset --soft HEAD~2 で、直近 2 つ分のコミットを取り消します。さきほどの表のとおり、--soft なので変更はステージングに残ったままです。あとはそれを 1 回コミットし直せば、2 つが 1 つにまとまります。難しい操作なしで、履歴をきれいにできます。
書き換えた履歴は --force-with-lease で上げ直す
reset で履歴を書き換えたので、GitHub に上げ直します。ただし、ここはさきほど「危険」と言った場面です。安全に、と頼みます。
歴史を書き換えたので GitHub に上げ直して。普通の push は断られるはずだから、安全な方法で。
書き換えた履歴は、GitHub 側の履歴と食い違います。普通に git push すると、non-fast-forward だと言って弾かれます。GitHub が「君の歴史は、こっちの歴史と枝分かれしている。このまま上書きしていいのか」と止めてくれているわけです。
ここで強制的に上書きする方法が 2 つあります。違いは安全装置の有無です。
| オプション | 動作 | リスク |
|---|---|---|
git push --force | 問答無用で上書きする | 自分が見ていない間に他人が push していたら、それを消す |
git push --force-with-lease | 自分が最後に見た時点と GitHub 側が一致するときだけ上書きする | 他人の push があれば弾かれるので、消さずにすむ |
--force-with-lease は、「自分が最後に確認したときから、向こうが動いていないことを確かめてから上書きする」という安全装置つきの強制 push です。第5回でも、作り直した歴史を上げるのに使いました。履歴を書き換えたあとの push は、いつも --force-with-lease。これを習慣にしておけば、自分の取り消しで他人の作業を吹き飛ばす事故を防げます。
git push --force-with-lease
ひとつ、正直な裏話があります。この実演の最初、普通の push が弾かれませんでした。書き換えたはずのコミットが、まだ push されていない手元だけのものだったので、GitHub から見れば単なる早送りで済んだのです。AI はそれに気づいて、「これでは force-with-lease の出番が作れない」と、わざと履歴を食い違わせる状況を作り直してから、もう一度実演し直しました。狙った場面が出ないとき、AI が黙って成功したことにせず、条件を整え直して見せてくれたのは、地味ですが信頼できる動きでした。
AI に頼むときの言い方
今日の作業も、毎回「指定した値」と「AI が勝手に決めた値」に分かれていました。
| 種別 | こちらが指定した値 | 指定しないと AI が勝手に決める値 |
|---|---|---|
| localStorage | 「リロードしても消えないように」「ローカルに保存」 | キー名 todos・データの形 (完了状態まで保存)・保存のタイミング |
| 取り消し | 「選択肢と適否を説明してからやって」 | push 済みかどうかの判断・revert を選ぶこと |
| reset | 「(A)(B)(C) の 3 パターンで実演」 | 各モードのコマンド・git status の見せ方 |
| まとめ | 「1 つにまとめて」 | reset --soft の件数・新しいコミットメッセージ |
| 上げ直し | 「安全な方法で」 | --force-with-lease を選ぶこと (--force を使わない判断) |
下 3 行のように、「3 パターンで」「安全な方法で」という方向だけ私が与えれば、具体的なコマンドは AI が正しく埋めます。逆に取り消しの行のように「説明してからやって」と頼めば、判断材料を出させてから自分で決められます。
まとめ
第 10 回として、保存機能を足しながら、取り消しとやり直しを実際のアプリで動かしました。
- localStorage 保存を
featureブランチで追加した。AI は完了状態まで保存する判断をした。 - push 済みのコミットは、
revert(履歴を残して打ち消す・安全) で戻す。resetは公開済みの履歴を壊すので、手元のまだ push していないコミットにだけ使う。この判断を AI に説明させてから決めた。 resetの 3 モードは、取り消した変更をどの領域まで戻すかの違い。git statusを見れば直感でわかる。--hardで消してもreflogから約 90 日は救出できる。- 散らかったコミットは
reset --softでまとめ、書き換えた履歴は--force-with-leaseで安全に上げ直す。
取り消しの怖さの大半は「戻せないかもしれない」という不安です。reflog という安全網と、「push 済みかどうかで revert か reset か」という判断軸を持てば、思い切って取り消せるようになります。次回は、このアプリに「すべて・未完了・完了」で絞り込むフィルタ機能を足し、それをプルリクエストの形で main に取り込みます。第6回で仕組みを見たプルリクエストを、今度は自分のアプリで回します。
なお、この記事で AI に渡したひと言は、すべて Claude(Sonnet) に実際に渡し、書いてあるとおりに動くことを確かめたものです。とくに取り消し系は、revert の選択・reset 3 モード・reflog 救出・--force-with-lease までを実機で順に再現しています。モデルやその日の状態によって、AI が勝手に決める値 (キー名やコミットメッセージなど) は変わります。指定しなかった部分は変わりうる、と思って読んでください。
パイソンエンジニア部

