4 ポイント 投稿者 GN⁺ 2025-01-18 | 1件のコメント | WhatsAppで共有
  • Git は、ミスの状況名がわからないと解決法を検索しにくいため、よくある復旧シナリオ別のコマンドをすぐ引いて使えるように整理した資料
  • リポジトリを大きく壊したり、削除・マージのミスを取り消したいときは git reflog で作業履歴を探し、git reset HEAD@{index}以前の状態に戻れる
  • 最後のコミットへの小さな修正やメッセージ変更は git commit --amend で素早く処理できるが、すでに公開・共有ブランチへプッシュしたコミットには使ってはいけない
  • 間違ったブランチにコミットした場合は、resetstashcheckoutstash pop の流れや cherry-pick で移せるが、プッシュ済みかどうかで安全な方法が変わる
  • リモートリポジトリの状態へ強制復旧する reset --hardgit clean -d --force は、追跡されていないファイルまで削除しうるため、実行前の確認が必要

Git のミスを復旧しにくい理由

  • Git はミスしやすく、問題を直すには必要な概念の名前をすでに知っていないと検索できないことが多い
  • この資料は、Git でよく遭遇する悪い状況を平易な英語とコマンド例で整理している
  • 完全なリファレンスではなく、同じ作業を処理する別の方法がある場合もある

リポジトリを過去の状態に戻す

  • リポジトリを大きく壊したり、削除した内容を復活させたいときは git reflog で作業履歴を確認する
git reflog


# すべてのブランチにまたがる Git 作業一覧を見られる


# 各項目は HEAD@{index} 形式のインデックスを持つ
git reset HEAD@{index}
  • reflog は次のような状況で使える
    • 誤って削除した内容の復旧
    • リポジトリを壊した試行の除去
    • 悪いマージ後の復旧
    • 実際に動いていた時点へ戻る

最後のコミットを修正する方法

  • 最後のコミット直後に小さな修正を追加したいなら git commit --amend --no-edit を使う
# 変更後
git add . # または個別ファイルを追加
git commit --amend --no-edit
  • このコマンドは最後のコミットに新しい変更を含める
  • 同じ作業を新しいコミットとして作成してから rebase -i でまとめることもできるが、amend のほうがはるかに速い
  • 公開・共有ブランチにすでにプッシュされたコミットには amend を使ってはいけない
  • 最後のコミットメッセージだけを変えたい場合は、次のコマンドを使う
git commit --amend


# 案内に従ってコミットメッセージを変更

間違ったブランチにコミットしてしまったとき

  • master に誤ってコミットしたが新しいブランチにあるべきだったなら、現在の状態から新しいブランチを作り、master の最後のコミットを削除する
git branch some-new-branch-name
git reset HEAD~ --hard
git checkout some-new-branch-name
  • この方法は、すでに公開・共有ブランチへプッシュしたコミットには適していない
  • 別の試行を先にしていた場合は、HEAD~ の代わりに HEAD@{number-of-commits-back} 形式が必要になることがある
  • 完全に間違ったブランチへコミットした場合は、reset --softstashcheckoutstash pop の流れで移せる
git reset HEAD~ --soft
git stash
git checkout name-of-the-correct-branch
git stash pop
git add . # または個別ファイルを追加
git commit -m "your message here"
  • 同じ状況で cherry-pick を使う方法もある
git checkout name-of-the-correct-branch
git cherry-pick master
git checkout master
git reset HEAD~ --hard

変更内容と過去のコミットを取り消す

  • ファイルを変更したのに git diff が空なら、すでにステージングしている可能性があるため --staged フラグが必要
git diff --staged
  • 数コミット前の変更を取り消したいなら git log でハッシュを見つけて git revert を実行する
git log
git revert [saved hash]
  • git revert はそのコミットを打ち消す新しいコミットを作る
  • コミット全体ではなく特定のファイルだけを戻すときは、ファイルの以前のバージョンをインデックスに載せてからコミットする流れを使う
git log
git checkout [saved hash] -- path/to/file
git commit -m "Wow, you don't have to copy-paste to undo"

リモートリポジトリの状態へ強制復旧する

  • リポジトリを消して再度クローンする代わりに、リモートリポジトリの状態に合わせる Git コマンドを使える
git fetch origin
git checkout master
git reset --hard origin/master
git clean -d --force
  • git reset --hard origin/master はブランチをリモートの master 状態に合わせる
  • git clean -d --force は追跡されていないファイルとディレクトリを削除する
  • 壊れたブランチごとに checkoutresetclean を繰り返す必要がある
  • これらの作業は破壊的で復旧不能なので、実行前に注意が必要

範囲と貢献

  • この資料は完全な Git リファレンスではなく、試行錯誤で見つけた実践的な復旧手順集
  • 翻訳追加を手伝いたいなら GitHub に PR を送れる

1件のコメント

 
GN⁺ 2025-01-18
Hacker Newsの意見
  • 変えたい点がいくつかある。常にgit checkoutの代わりに**git switchを使い、reset --hard**はできるだけ避けると思う
    たとえば「新しいブランチにあるべきコミットを誤ってmasterにしてしまった場合」は、git branch some-new-branch-namegit switch -d HEAD~git switch -C mastergit switch some-new-branch-nameの順で対処できる
    「間違ったブランチにコミットした場合」も、正しいブランチへgit switchした後、git cherry-pick mastergit switch -d master~git switch -C masterで対処すると思う
    「すべて諦めてリモートの状態に戻す」場合も、git fetch origingit restore -WS .git clean -d --forcegit switch -d origin/mastergit switch -C masterのように構成すると思う

    • Gitの内部モデルはblob、コミットツリー、コミットポインタで美しいのに、コマンドラインインターフェースとの隔たりが大きすぎる
      Gitのモデルをよく理解していても、こうしたレシピは直感的ではなく、各コマンド特有の挙動まで知っている必要がある
      たとえばgit switch -c some-new-branch-nameはすでにあるが、ブランチだけを別のコミットへ移すgit move-branch master HEAD~のようなコマンドのほうが直感的だと思う
    • git checkoutの代わりにgit switchgit restoreを使う方向に反対するつもりはないが、これらのコマンドはGit v2.23で導入され、そのリリースは約5年前のものだ
      ヘルプページには今でもTHIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.という警告が入っている
      もちろんこの警告はコマンドができたほぼ当初からあり、5年も経ったなら、もう実験的と呼ばなくてもよい時期になっているように思う
      それでもgit checkoutのほうが後方互換性が高く、もともとこのWebサイトが作られた時期のGitの使い方を反映しているとも見なせる
      https://github.com/git/git/blob/757161efcca150a9a96b312d9e78...
      https://github.com/git/git/releases/tag/v2.23.0
      https://github.com/git/git/commit/4e43b7ff1ea4b6f16b93a432b6...
    • 変更をステージング領域に載せるとき、デフォルトで**git add .**を教えるのはあまりよくない
      特定のファイルを明示して追加するほうが、あとで「あ、やってしまった」となる余地が少ない
    • なぜこういう方法を勧めるのか、根拠が気になる
      switchcheckoutより優れている理由と、reset --hardを使うべきでない理由を知りたい
    • reset --hardを避けろ」というのがどういう意味なのかわからない
      実際になぜ不十分なのか気になるし、alias git-restore-file='git restore --source=HEAD --'と一緒にかなり頻繁に使っているが、うまく動いているように思う
  • Gitを学ぶときの基本ルートとして、GUIクライアントを勧め始めるべきだ
    そうすればこうした問題の3分の1は解決し、さらに別の3分の1はそもそも発生しないはずだ
    後でコマンドラインのほうが速いと判断したら、そのとき使えばよい。まずは人々がツリーとやり取りする方法を視覚的に見る必要がある
    個人的にはfork.devが好きだが、最近のクライアントはだいたい似ている

    • GUIがGitにより良いユーザー体験をもたらす場合が多いという点には同意する
      Magitを使うと、「cherry-pickの中断」のような操作をインターフェース内で見つけられ、他の「Xを中断」操作と同じショートカットを使う
      Gitのコマンドラインだけを使わなければならなかったら、どこから始めればよいのか見当がつかなかったと思う
      インタラクティブリベース中に削除してはいけないコミットを消したこともあるが、リベース開始時点ごとにreflogからアクセス可能なスナップショットが作られるので、誤ったリベースを戻すのは安全だ
      Magitのreflog UIはlog UIと同じなので初見でも迷わなかったが、Git CLIだったら何が起きているのかわからなかった可能性が高い
    • Git互換の代替としてjjを勧め始めた
      jjを引数なしで実行すると、コミットツリーの関連部分と現在位置を表示してくれる点がよい
      今作業中のコミット、紐づいたブランチと履歴、リポジトリ内の他のアクティブなブランチ、各コミットのメッセージと変更の有無を一目で見られるので、方向を立て直すのに素晴らしい
    • Git初心者にはいつも、自分に合うUIを見つけてコマンドラインよりそれを使うよう勧めている
      だがほとんど誰も聞かず、結局CLIで苦労し、ブランチを壊し、mainに遅れを取り、rebaseを怖がるようになる
      UIならドラッグ&ドロップやそれに近い方法で解決できることが多いのに、一種の自虐のように見える
    • ツリーとやり取りする方法を視覚的に見る必要があるなら、同僚たちの役に立ったインタラクティブチュートリアルがある: https://learngitbranching.js.org/
    • 以前バージョン管理のGUIフロントエンドを使った経験では、すべてがうまく回っているときは問題ないが、一度こじれたものを整理しなければならなくなると、誰も助けにくかった
      インターフェースがひどくても、チームの他の人たちが使っているものと同じインターフェースを使うほうが意思疎通しやすく、オンラインのトラブルシューティング資料もたいてい「普通のやり方」を前提にしているので活用しやすかった
  • 2025年4月でGitが20周年だなんて、かなり不思議な感じがする
    当時その現場にいて、SVNやCVSを置き換えるために git、darcs、bazaar のどれを学ぶべきか悩んでいたのを覚えている
    Mercurialも試したかどうかは分からない
    「GitHub効果」が、バージョン管理システムの分野で新規参入者の必要性を事実上消してしまったのか気になる
    ある時点で、ヤクの毛刈りは十分に終わっていたのかもしれない

    • GitHub効果は、あらゆるネットワーク効果と同じく実際に存在するが、だからといって改善が不可能という意味ではない
      完全にjjへ乗り換えたし、Git互換性があるので、ほかの人が一緒に乗り換えてくれるのを待つ必要もない
      GitHubのさまざまな面にますます不満を感じているので、いつか皆もそこを越えてくれればと思うが、そちらは先へ進む道がそれほど単純には見えない
    • SVNはいつもちゃんと動いていた
      SVNは直感的で、99%のケースでは十分うまく動くので、人にわざわざ「教える」必要がない
      みんなで1337ハッカーごっこをやめて、Gitは大多数の人にとって過剰なツールだと認めてほしい
    • どんな改善であれ、少なくともGit互換である必要があるだろう。たとえばjjがそうだ
  • Gitが何をしているのかについてのメンタルモデルはかなりしっかり持っていると思うが、コマンドが少し複雑になるだけで引数を覚えていられない
    コマンドは発見しやすくもないし、覚えやすくもない
    テキストUIが悪いのか、それともツリーを操作する作業をテキストで説明すること自体が難しいのか分からない

  • このスレッドでhgとMercurialの検索結果が出てきてうれしい
    Mercurialが分散バージョン管理戦争に負けたことはいまだに信じられないし、より良いツールだと思っている

    • 今の基準でもMercurialは疑いようもなくより良いツールだ
      使えて、安定していて、実戦で検証済みで、活発にサポートされている
      「自分が見たい変化になれ」に近い話なので、そのまま使えばいい
      負けたというような言い方は逆効果が大きく、そうした言葉がもたらす暗黙の抑制がMercurialの採用率を低く保っている
      ホスティングは少なくともSourcehutとheptapod.hostで可能だ
      個人用のHeptapodインスタンスも運用しているが、GitlabフォークにMercurialの直接サポートが入った形で、普通にうまく動いている
    • 興味深いけれど、GitHubリンクがあるのか気になる
    • Mercurialはより遅いツールだ
    • Mercurialのホスティングが必要なら、このホスティングを使える: https://hg.reactionary.software/
  • Gitはいろいろな面で直感に反していて、結局なかなか頭に入ってこない技術の一つだった
    長く使ってきたのでなければ、ほぼすべての作業でGoogleを探すかmanページを見ることになる可能性が高い

    • SVNから始めてGitへ移ってきたので、違いを学ぶ必要はあったが、多くの面でより簡単になったと感じた
      特に大きなコードベースをSVNで多数のブランチとして管理し、再びマージしようとする恐怖を経験した後だと、Gitはその部分ではばかげているほどはるかに優れていることが分かる
      最初からGitに入る人には学習曲線がずっと大きいかもしれないが、人々がどこで詰まるのか、いまだに理解しづらいことがある
    • 昨日、履歴からファイルを削除しようとした
      組み込み方式の**filter-branch**を使うと、長い遅延とともにControl+Cを押してfilter-repoというサードパーティ製Pythonスクリプトをダウンロードしろという警告が出る
  • 自慢できることではないが、一番よく使う「やらかした」Git操作は、ローカルリポジトリを削除して再クローンし、変更内容を再適用することだ
    95%はとてもよく効くし、残りはDevOps担当者に助けを求める

    • Gitをほぼ15年使っていて、Gitを内部的に使って特定の結果を出すプログラム/製品も2回作ったことがあるが、それでもGitで少し危ない作業をする前にはcp -R .git ../git-backupのようなことをしておく場合がある
      大きく失敗しすぎたら、.gitディレクトリ全体を以前のコピーに置き換えればよいからだ
      特定の作業や作業のまとまりを正しく取り消す方法を探すより、ずっと速い
    • あなただけではない: https://xkcd.com/1597/
  • Gitについて人々にぜひ体得してほしいのは、Gitが追記専用データストアだという点だ
    作業をリポジトリに入れるにはコミットすればよく、一度入った作業はGitコマンドでは削除できない
    reflogもこの原理で動作している
    何をしても以前のブランチ状態に戻って、データストアから作業を復元できる
    しかしコミットしていなければGitは助けられないので、とにかくコミットすべきだ
    常にコミットし、負担が大きすぎる、あるいはつい忘れてしまうなら、ツールを直すべきだ
    一日中コードを書いているなら、一般的には少なくとも1時間に1回はコミットすべきだ

  • 些細な話だが、git reflogを見るたびにre-flogのように見える
    だからこのコマンドをうまく覚えられないのかもしれないし、無意識に「reference log」と考えられていないようだ
    代わりに、Gitに何かをもう一度安く売れと命じているような感じがする

  • Gitユーザーではありませんが、こういうものを見ると、git commit 自体には意味がなく、重要なのはコミットがpushされる、またはmergeされる時点だけなのだという考えが強まります
    テキストファイルの保存に似ています
    ファイルを保存するたびに短いメッセージを書くわけではないので、そのまま流せばよいのです

    • つらいデバッグの旅の末に生まれた変更で、ほかの人が後でまた参照する可能性があるならメッセージを書きます
      短くではなく、変更が小さいほど説明が長くなることもあります
      影響の大きい2〜3行の変更に、2〜3段落を書いたこともあります
    • ローカルコミットとリモートコミットは目的が違う、ということが非常にはっきりしているように見えます
      ローカルコミットは、すべてがコンパイルされ動作する時点でチェックポイントを作る方法です
      そうすれば、変更をstashしたり、失敗したときに動いていた状態へ戻したりできます
      pushする前にresetして適切なコミットに分ければ、リモートの履歴はきれいに保たれ、wip のダミーにはなりません
      あるいはPRをsquash mergeして、両方の利点を得ることもできます
    • コミットはGitに内部的にファイルのコピーを作らせる操作なので、とても重要です
      ただし、more fixes のような意味のないメッセージを入力する必要はありません
      たいていは (WIP) 機能名 のような最初のコミットを作り、満足するまでamendコミットを繰り返したあと、最後にメッセージから (WIP) を取り除きます
    • テキストファイルの保存と比べていますが、眠るたびにその日について短い文章を書く人も多くいます
      Gitのリビジョンログも、そうした日記に似ていると思います
      別のTODO文書より整理されており、すでにやったことを読みやすい形で説明するよう促してくれます
      どこまで戻すか判断するときに、そのコミット時点で何を考えていたのかを知るのは役に立つことがあり、アプローチが間違っていたなら事後分析にも役立ちます
      mergeやpushされていない作業が、目的の付いた個別の変更として整理されていれば、そのうち一部は良く、一部は悪い場合に、良い部分だけを残すのがずっと簡単になります
    • pushするまではすべてローカルなので、好きなようにしてかまいません
      それでも技術的にコミットに意味がないわけではありません
      コミットがなければ、pushしたりmergeしたりする対象もありません
      非技術的にも、なぜ何かを変えるのかを平文で記録するのは非常に有用です
      そうした説明をあまりに頻繁に書くことになるなら、「変更」の単位をもっと狭めるか、同時に触る作業をもっと厳選すべきです
      Gitを使っていないなら、バージョン管理と変更管理を何で行っているのか気になります