はじめに
今回は実務経験半年の私が、実務での失敗談を通じて、チーム開発では必ずと言って良いほど使われるGitの使い方についてまとめていきます。なんとなくの理解で作業をすることの愚かさを身をもって体験しましたので、その後の学習の記録を共有いたします。ぜひ読者の方も何が間違いでどうすればよかったか考えながらお読みください!
間違いなどありましたら、コメントでご指摘ください!
前提
- ブランチの概念は理解している
- 作業をリモートに
push
することはできる - コマンドの細かい仕様は理解していない
何をしたのか
※以下は誤った手順を含みます。絶対にそのまま真似しないでください。
-
git switch -c feature-xxx
でブランチを切り作業をする -
git add xxx
で作業ファイルをステージング -
git commit -m'commit message'
でコミット -
git push origin feature-xxx
でリモートにプッシュ - 2~4繰り返し
-
git add xxx
で誤ってコミットしたくないファイルをステージング git reset --hard HEAD~
git cherry-pick <リモートの最新のコミットID>
-
git add xxx
で目的のファイルをステージング git commit -m'commit message'
-
git push origin feature-xxx
←pushできない -
git pull origin feature-xxx
←pullできない git pull --rebase origin feature-xxx
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 を 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 --hard
やrebase
で書き換えた場合、履歴がズレてpush
できなくなることがあります。
-
push
してから履歴を壊すのは基本NG - どうしても必要な場合は
--force
が必要だが、チーム開発では注意が必要
まとめ
今回の失敗から学んだことは、Gitの履歴操作は慎重に行うべきということです。特に reset --hard
や cherry-pick
は強力な分、使い方を誤ると履歴が壊れ、push
やpull
ができなくなる原因になります。
教訓
-
reset --hard
は履歴も変更も消える。ほぼ最終手段 -
cherry-pick
は見た目が同じでも別履歴になる -
pull
できないのは履歴がズレているサイン。--rebase
で整える - 間違ったら
reset --soft
やreflog
で安全に戻す
安全な対応まとめ
状況 | 正しい対応 |
---|---|
add だけ戻したい |
git reset HEAD <ファイル名> |
コミットをやり直したい |
git commit --amend or reset --soft
|
push 前にズレを整えたい | git pull --rebase |
消えた履歴を戻したい |
git reflog → reset
|
Gitは「積み重ねる」ツールです。焦らず、一歩ずつ修正するのが一番の近道でした。
参考