😀

実務の失敗談から学ぶ!git reset,cherry-pick,rebaseを完璧に理解する

に公開

はじめに

今回は実務経験半年の私が、実務での失敗談を通じて、チーム開発では必ずと言って良いほど使われるGitの使い方についてまとめていきます。なんとなくの理解で作業をすることの愚かさを身をもって体験しましたので、その後の学習の記録を共有いたします。ぜひ読者の方も何が間違いでどうすればよかったか考えながらお読みください!
間違いなどありましたら、コメントでご指摘ください!

前提

  • ブランチの概念は理解している
  • 作業をリモートにpushすることはできる
  • コマンドの細かい仕様は理解していない

何をしたのか

※以下は誤った手順を含みます。絶対にそのまま真似しないでください。

  1. git switch -c feature-xxxでブランチを切り作業をする
  2. git add xxxで作業ファイルをステージング
  3. git commit -m'commit message'でコミット
  4. git push origin feature-xxxでリモートにプッシュ
  5. 2~4繰り返し
  6. git add xxxで誤ってコミットしたくないファイルをステージング
  7. git reset --hard HEAD~
  8. git cherry-pick <リモートの最新のコミットID>
  9. git add xxxで目的のファイルをステージング
  10. git commit -m'commit message'
  11. git push origin feature-xxx←pushできない
  12. git pull origin feature-xxx←pullできない
  13. git pull --rebase origin feature-xxx
  14. git push origin feature-xxx

それぞれのコマンドの意味

git switch

ブランチを切り替えるコマンド。
-c オプションで新しいブランチを作成して同時に切り替える。

git reset

git reset は、HEAD(現在位置)を別のコミットに移動し、その際にステージ(index)や作業ツリー(working directory)もどう扱うかをオプションで指定するコマンドです。


git reset --soft

  • HEAD だけを移動する
  • ステージ(index)と作業ツリーは そのまま
  • 主な用途:直前のコミットをなかったことにして、やり直したいとき

使いどころ:

  • git commit --amend したいとき
  • 誤ってコミットしたけど、メッセージなどを直したいとき

git reset --mixed(デフォルト)

  • HEAD を移動し、ステージ(index)をリセット
  • 作業ツリーはそのまま(ファイルの変更内容は残る)

使いどころ:

  • 一度コミットしたけど「やっぱり add するファイルを見直したい」とき
  • ステージしたファイルを 一括で解除したいとき

git reset --hard

  • HEAD・ステージ・作業ツリーすべてを指定コミットの状態に戻す
  • 変更内容は 完全に破棄される(reflog以外で復元困難)

使いどころ(慎重に!):

  • 明らかに不要なコミットとファイル変更を完全に取り除きたいとき
  • 作業中のごちゃごちゃを一旦すべて消したいとき(ただし注意)

重要:--hard はローカルでしか使わないようにしよう。

誤って共有ブランチで使うと他人の履歴を壊すことになります。

git cherry-pick

指定したコミットだけを現在のブランチに取り込む。

  • 内容が同じでも新しいコミット(別のコミットID)として追加される
  • このため push 時に「リモートと履歴が違う」とされ、拒否される

git push

ローカルの変更をリモートに反映する。

  • リモートに同名ブランチがあり、かつ履歴が異なる場合は拒否される
  • これは Git が「上書きしていいのか判断できない」ため

git pull

リモートの変更をローカルに取り込むコマンド。

  • デフォルトでは git fetch + git merge の動作(= マージ型pull)
  • 履歴がローカルとリモートで分かれている場合、明示的な統合方針が必要

pull における merge と rebase の違い

比較項目 git pull(マージ git pull --rebase
履歴の構造 分岐が残る 直線的に並ぶ
コミットの数 そのまま(マージコミット含む) コミットが付け直される(hashが変わる)
履歴の見やすさ ごちゃつくことがある きれいで追いやすい
コンフリクト解消 1回で済むことが多い 各コミットごとに必要なこともある
推奨場面 チーム開発で複数人が同時に作業するブランチ 個人開発やfeatureブランチ、レビュー前の整理用

では今回の事例では何が起こったのか、次節でイメージ図とともに解説していきます。

何が起こったのか

前提の状態(正常な履歴)

まず、以下のようにローカルとリモートが同期していたとします。

A---B---C   ← origin/feature-xxx
A---B---C   ← local HEAD(feature-xxx)

① git reset --hard HEAD~ を実行(Cを削除)

この操作で 1つ前(B)まで履歴を巻き戻し、コミットCもファイルの変更も 完全に消える

A---B---C   ← origin/feature-xxx(リモートにはまだある)
A---B       ← local HEAD(Cが消えた)

※ この時点でローカルとリモートの履歴が 分岐(diverged) してしまっています。


② git cherry-pick <CのコミットID> を実行

ローカルで、リモートの C を cherry-pick(適用)した状態

A---B---C   ← origin/feature-xxx
A---B---C'  ← local HEAD(C' は cherry-pick された新しいコミット)

※ C と C' は内容が同じでも コミットハッシュが異なる 別履歴。


③ ここで git pull すると…?

Gitは以下のようなエラーを出します

fatal: Need to specify how to reconcile divergent branches

git pull は内部的に git fetch + git mergeを実行しますが、ローカルとリモートの履歴が異なると、Gitは自動でどちらを優先すべきか決められません。
よって自分でどちらを使ってpullするかを指定しなければなりません。
この状態で git pull しようとした時の merge と rebase の違いを以下に図解します。


git pull --rebase を実行した場合

A---B---C---C''  ← local HEAD(C' を C の後ろに並べ直し)
A---B---C        ← origin/feature-xxx
  • C' が C'' に置き換えられた
  • 履歴が まっすぐに整い、push 可能に

git pull --merge を実行した場合

A---B---C-------M  ← local HEAD(mergeコミット)
         \     /
          C'---     ← ローカル履歴に分岐と統合あり
  • M は Git による自動マージコミット
  • 履歴が複雑化するが、push は可能になる

事の顛末

  • reset --hard はコミットと変更内容を完全に消すため、履歴を壊すリスクが高い
  • cherry-pick は内容を戻せても、履歴上は別コミット扱いになる
  • 結果として push/pull 時に分岐とみなされ、エラーが出る
  • 解決するには git pull --rebase または --merge を明示する

どうすればよかったか

では今回の事例ではどうすればよかったのでしょうか?

まず git reset --hard を使わない

今回のトラブルの根本原因は、git reset --hard HEAD~ を実行してローカルの最新コミットと変更内容を完全に消してしまったことです。
この操作は 履歴も作業内容も復元しづらくなるため、非常に危険です。


ステージングの取り消しは git reset HEAD <ファイル名>

もし git add を誤って実行してしまった場合は、以下のようにステージ(add)だけを解除すればよかったのです。

git reset HEAD ファイル名
  • ファイルの内容はそのまま残る
  • コミットもしない、履歴も壊れない
  • 単に「ステージ解除」するだけなので、安全です

誤ってコミットした場合は --soft で巻き戻す

もしcommitまでしてしまった場合でも、--softを使えば変更内容を保持したままコミットを取り消せます。

git reset --soft HEAD~
  • 最新のコミットを取り消すが、内容はステージに残る
  • 再度修正してコミットし直せばOK

内容やメッセージを直したいだけなら git commit --amend

git commit --amend
  • メッセージの書き換え、ファイルの差し替えができる
  • 履歴が1コミットで済むため、見た目がきれい

push前にリモートの変更を取り込む(pull --rebase

git pull --rebase origin feature-xxx
  • ローカルとリモートの履歴がズレている場合、これでリモートの履歴を取り込んでから、自分の変更を後ろに付け直す
  • 履歴が「まっすぐ」になり、pushでの衝突も起きづらくなる

消したコミットを復元したいならgit reflog

万が一reset --hardをしてしまった場合でも、Gitは内部で履歴を一時的に保存しています。
直後であればgit reflogでコミットの履歴を確認し、元に戻すことも可能です。

git reflog
git reset --hard <元のコミットID>

push 後の履歴修正は原則避ける

リモートにpushしたコミットをreset --hardrebaseで書き換えた場合、履歴がズレてpushできなくなることがあります。

  • pushしてから履歴を壊すのは基本NG
  • どうしても必要な場合は--forceが必要だが、チーム開発では注意が必要

まとめ

今回の失敗から学んだことは、Gitの履歴操作は慎重に行うべきということです。特に reset --hardcherry-pick は強力な分、使い方を誤ると履歴が壊れ、pushpullができなくなる原因になります。


教訓

  • reset --hard は履歴も変更も消える。ほぼ最終手段
  • cherry-pick は見た目が同じでも別履歴になる
  • pull できないのは履歴がズレているサイン。--rebase で整える
  • 間違ったら reset --softreflog で安全に戻す

安全な対応まとめ

状況 正しい対応
add だけ戻したい git reset HEAD <ファイル名>
コミットをやり直したい git commit --amend or reset --soft
push 前にズレを整えたい git pull --rebase
消えた履歴を戻したい git reflogreset

Gitは「積み重ねる」ツールです。焦らず、一歩ずつ修正するのが一番の近道でした。

参考

https://git-scm.com/docs

Discussion