Git入門:バージョン管理のきほん
必要なコミットだけ取り込む(cherry-pick)
このレッスンで分かること
- cherry-pick が「コミットの移動」ではなく「変更内容のコピー」である仕組み
- 緊急のバグ修正を取り込むときの具体的な手順とコンフリクトへの対処
- cherry-pick を使ってよい場面と、マージや rebase を選ぶべき場面の境界
ブランチ全体ではなく 1 コミットだけ欲しいとき
マージはブランチの成果をまるごと合流させます。ふだんはそれで足ります。
ところが、こういう場面があります。feature-search ブランチで検索機能を開発中で、まだ完成していません。しかしその作業の途中で、ついでに直したバグ修正のコミットが 1 つ入っています。そのバグは本番で発生しているので、今すぐ main に入れたい。しかし検索機能は未完成なので、ブランチごとマージするわけにはいきません。
このとき使うのが git cherry-pick です。ブランチから欲しいコミットだけを摘み取ってきます。名前の通り、さくらんぼをひと粒だけ摘む操作です。
| やりたいこと | 使うもの |
|---|---|
| ブランチの成果をまるごと合流させる | git merge |
| ブランチの土台を最新に置き換える | git rebase |
| 特定のコミット 1 つだけ持ってくる | git cherry-pick |
動かしてみる
まず現状を確認します。git log --oneline --all --graph で、全ブランチの形が一度に見えます。
ターミナル
$ git log --oneline --all --graph
* 7d2e918 (feature-search) 検索フォームの見た目を整える
* c4a1b03 日付のゼロ埋めが抜けていたのを修正
* 8e5f7a2 検索 API の下書き
* 3b9c0d1 (HEAD -> main) ユーザー一覧のページングc4a1b03 の日付の修正だけを main に入れたいとします。手順は 2 つです。取り込みたいブランチに移動して、コミットのハッシュを指定します。
ターミナル
$ git switch main
Already on 'main'
$ git cherry-pick c4a1b03
[main 5f8a2c1] 日付のゼロ埋めが抜けていたのを修正
Date: Mon Mar 10 14:22:03 2025 +0900
1 file changed, 3 insertions(+), 1 deletion(-)結果を見ます。
ターミナル
$ git log --oneline --all --graph
* 7d2e918 (feature-search) 検索フォームの見た目を整える
* c4a1b03 日付のゼロ埋めが抜けていたのを修正
* 8e5f7a2 検索 API の下書き
| * 5f8a2c1 (HEAD -> main) 日付のゼロ埋めが抜けていたのを修正
|/
* 3b9c0d1 ユーザー一覧のページングコピーであって移動ではない
いま起きたことをよく見てください。
feature-search の c4a1b03 は消えていません。そこに残ったままです。main 側には 5f8a2c1 という別のハッシュを持つ新しいコミットができました。
つまり cherry-pick は、コミットを引っ越しさせるのではなく、そのコミットが持っている変更内容を取り出して、いまいるブランチの先端に新しいコミットとして積み直す操作です。
| 項目 | 元のコミット | cherry-pick でできたコミット |
|---|---|---|
| ハッシュ | c4a1b03 | 5f8a2c1 (別物) |
| 変更内容 | 同じ | 同じ |
| コミットメッセージ | 同じ | 同じ |
| 作成者 | 元の人のまま | 元の人のまま |
| コミット日時 | 元のまま | 実行した日時 |
ハッシュが変わるのは、Git のコミットが「変更内容」だけでなく「どのコミットの次に来るか」も含めて計算されるためです。積む場所が違えば、中身が同じでも別のコミットになります。
もうひとつ、細かいですが重要な点があります。作成者 (Author) は元の人のまま引き継がれますが、そのコミットを実際に作った人 (Committer) は cherry-pick を実行したあなたになります。誰が書いたコードかという情報は消えません。
ターミナル
$ git log -1 --format="Author: %an / Committer: %cn"
Author: Hanako Suzuki / Committer: Taro Yamadaハッシュが違うということは、Git から見て 2 つは別のコミットです。あとで
feature-searchをmainにマージすると、同じ変更が 2 回現れることになります。多くの場合 Git は賢く処理してくれますが、コンフリクトの原因になることもあります。
よく使うオプション
複数まとめて取り込む
コミットを並べれば、その順に取り込まれます。
ターミナル
$ git cherry-pick c4a1b03 8e5f7a2連続した範囲を指定することもできます。A..B は「A より後から B まで」という意味で、A 自身は含まれません。
ターミナル
$ git cherry-pick 8e5f7a2..7d2e918コミットせずに変更だけ持ってくる
-n をつけると、変更をワークツリーとステージに置くところで止まります。メッセージを書き直したいときや、複数の cherry-pick を 1 つのコミットにまとめたいときに使います。
ターミナル
$ git cherry-pick -n c4a1b03
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/utils/date.js
$ git commit -m "日付フォーマットの不具合を修正 (feature-search から取り込み)"どこから来たかを記録する
-x をつけると、コミットメッセージの末尾に元のコミットのハッシュが自動で追記されます。
ターミナル
$ git cherry-pick -x c4a1b03できたコミットのメッセージは次のようになります。
プレーンテキスト
日付のゼロ埋めが抜けていたのを修正
(cherry picked from commit c4a1b03f7e6d5c4b3a2918f7e6d5c4b3a2918f7e)あとから「この修正はどこから持ってきたのか」を追えるので、チームで使うときは -x をつける習慣にしておくと後が楽です。
コンフリクトしたときの手順
cherry-pick は、取り込み先のコードが元のブランチと違っていると、当然ぶつかります。
ターミナル
$ git cherry-pick c4a1b03
Auto-merging src/utils/date.js
CONFLICT (content): Merge conflict in src/utils/date.js
error: could not apply c4a1b03... 日付のゼロ埋めが抜けていたのを修正
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".慌てる必要はありません。Git が次に何をすればいいかを全部書いてくれています。
| コマンド | 何をするか |
|---|---|
git cherry-pick --continue | 解決した内容でコミットして先へ進む |
git cherry-pick --skip | このコミットを飛ばす |
git cherry-pick --abort | 実行前の状態に完全に戻す |
解決する流れは、通常のコンフリクトと同じです。まず git status でどのファイルが残っているかを確認します。
ターミナル
$ git status
On branch main
You are currently cherry-picking commit c4a1b03.
(fix conflicts and run "git cherry-pick --continue")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/utils/date.jsファイルを開いて <<<<<<< から >>>>>>> までのマーカーを消し、正しい形に直します。直したら git add して続行します。
ターミナル
$ git add src/utils/date.js
$ git cherry-pick --continueエディタが開いてコミットメッセージの確認を求められます。そのまま保存して閉じれば完了です。
途中で分からなくなったら、迷わず
git cherry-pick --abortを打ってください。cherry-pick を始める前の状態に完全に戻ります。中途半端に解決したまま放置するほうが、はるかに厄介な状態を作ります。
実際の流れをひと通り追う
本番で動いているのが v1.2.0 のタグがついた状態で、そこにバグが見つかった場面を考えます。main はすでに次のリリースに向けて先へ進んでいて、そのままリリースするわけにはいきません。
まず、修正はふだん通り main で作ります。
ターミナル
$ git switch main
$ git switch -c fix-date-format修正してコミットします。
ターミナル
$ git add src/utils/date.js
$ git commit -m "日付のゼロ埋めが抜けていたのを修正"
[fix-date-format a1c9e4d] 日付のゼロ埋めが抜けていたのを修正
1 file changed, 3 insertions(+), 1 deletion(-)これをレビューしてもらい、main に取り込みます。ここまでは通常の開発と同じです。
次に、本番用のリリースブランチにこの 1 コミットだけを持っていきます。
ターミナル
$ git switch release-1.2
$ git cherry-pick -x a1c9e4d
[release-1.2 6b3f8e0] 日付のゼロ埋めが抜けていたのを修正
1 file changed, 3 insertions(+), 1 deletion(-)main の他の変更は一切入っていません。動作を確認したら、修正版としてタグを打ちます。
ターミナル
$ git tag -a v1.2.1 -m "日付表示の不具合を修正"
$ git push origin release-1.2 v1.2.1同じ修正が main と release-1.2 の両方に、別々のコミットとして存在することになります。これは重複ではなく、意図した状態です。どちらのブランチをリリースしても修正が入っている、という形を作っています。
これが cherry-pick のいちばん素直な使い道です。「本流の開発は止めずに、安定版にだけ修正を届ける」という要求に、他の道具では答えられません。タグの打ち方は タグでリリースに印をつける で扱いました。
やりがちな失敗と、使ってよい場面
いちばん多い失敗は、同期の道具として cherry-pick を使い続けることです。
develop から main へ 20 個のコミットを 1 つずつ cherry-pick する、というような使い方をすると、両方のブランチに同じ変更を持つ別々のコミットが大量に並びます。履歴を見ても、どちらが本物なのか分かりません。あとでマージしたときにコンフリクトが噴出します。
もうひとつは、依存関係を見落とす失敗です。修正コミット c4a1b03 が、その 1 つ前の 8e5f7a2 で追加された関数を呼んでいたとします。c4a1b03 だけを cherry-pick すると、存在しない関数を呼ぶコードが main に入ります。cherry-pick 自体は成功し、コンフリクトも起きません。壊れるのは実行したときです。
ターミナル
$ git show c4a1b03取り込む前に必ず中身を見て、そのコミットが単独で成立するかを確認してください。
使ってよい場面をまとめます。
- 本番で起きているバグの修正だけを、未完成のブランチから急いで持ってきたい
- リリース済みの安定版ブランチに、修正パッチだけを当てたい
- 間違ったブランチでコミットしてしまったので、正しいブランチへ移したい
3 つ目は、cherry-pick したあと元のブランチ側で git reset するか git revert して打ち消します。両方に残さないことが大切です。
これ以外の、ブランチ全体を取り込みたい場面ではマージを選んでください。cherry-pick は例外的な操作であって、日常の同期手段ではありません。
- cherry-pick はコミットの移動ではなくコピー。元のコミットは残り、取り込み先には別ハッシュの新しいコミットができる
- コンフリクトしたら
--continueで進め、迷ったら--abortで完全に元へ戻す - 単独で成立しないコミットを取り込むと、コンフリクトなしで壊れたコードが入る。
git showで中身を確認してから実行する - 日常の同期はマージで行う。cherry-pick は緊急の修正取り込みなど例外的な場面に限定する