DevToolBox免费
博客

Git cherry-pick、revert 和 reset 详解

10 分钟阅读作者 DevToolBox

理解 git cherry-pickgit revertgit reset 对于任何需要在分支之间移动提交、撤销错误或重写历史的开发者来说都是必不可少的。本综合指南深入讲解每个命令,提供实际示例、冲突解决策略和真实工作流程。无论你需要回移热修复、撤销错误部署还是清理混乱的历史,你都能在这里找到正确的方法。

使用我们的 Git 命令生成器交互式生成命令 →

Cherry-Pick 基础:应用单个提交

git cherry-pick 获取一个或多个现有提交,并将它们作为新提交应用到当前分支。与 merge 或 rebase 不同,cherry-pick 允许你选择特定的提交,而不是集成整个分支。原始提交保持不变——cherry-pick 创建一个全新的提交,哈希值不同但变更相同。

# Apply a single commit to the current branch
git cherry-pick abc1234

# Apply multiple commits at once
git cherry-pick abc1234 def5678 ghi9012

# Cherry-pick the tip of another branch
git cherry-pick feature-branch

何时使用 cherry-pick:

  • 将开发分支的 bug 修复回移到发布分支
  • 拉取单个功能提交而不合并无关变更
  • 从已删除或废弃的分支中恢复提交
  • 将修复应用到多个维护分支
# Example: Backport a fix from develop to release
git checkout release/1.0
git cherry-pick abc1234      # the fix commit on develop
git push origin release/1.0

Cherry-Pick 选项和标志

git cherry-pick 支持多个有用的标志,控制提交的应用方式。这些选项让你精细控制 cherry-pick 过程。

--no-commit (-n)

--no-commit (-n):将变更应用到工作目录和曲存区,但不创建提交。这让你可以将多个 cherry-pick 合并为一个提交,或在提交前修改变更。

# Apply changes without committing
git cherry-pick --no-commit abc1234

# Combine multiple cherry-picks into one commit
git cherry-pick --no-commit abc1234
git cherry-pick --no-commit def5678
git commit -m "Backport: fix login and auth bugs"

# Check what changed before committing
git cherry-pick -n abc1234
git diff --staged

-x (append source reference)

-x:在提交消息中追加 "(cherry picked from commit ...)" 行。这对于跟踪 cherry-pick 的来源非常有用,尤其是跨发布分支时。

# Cherry-pick with source tracking
git cherry-pick -x abc1234

# Resulting commit message:
# Fix: prevent null pointer in auth module
#
# (cherry picked from commit abc1234567890abcdef)

--edit (-e)

--edit (-e):在创建提交前打开提交消息编辑器。当你需要调整提交消息以适应目标分支的上下文时使用。

# Cherry-pick and edit the commit message
git cherry-pick --edit abc1234

A..B range

A..B 范围:Cherry-pick 从 A(不包含)到 B(包含)的一系列提交。注意 A 不会被包含——它定义起始点。

# Cherry-pick commits from abc1234 (exclusive) to ghi9012 (inclusive)
# This picks def5678 and ghi9012
git cherry-pick abc1234..ghi9012

# Equivalent to:
git cherry-pick def5678 ghi9012

# Cherry-pick a range with source tracking
git cherry-pick -x abc1234..ghi9012

--signoff (-s)

--signoff (-s):在提交消息中添加 "Signed-off-by" 尾部,这在开源项目中通常是必需的。

# Cherry-pick with sign-off
git cherry-pick --signoff abc1234

# Resulting message includes:
# Signed-off-by: Your Name <you@example.com>

解决 Cherry-Pick 冲突

Cherry-pick 冲突发生在 cherry-pick 的提交中的变更与目标分支上已有的变更重叠时。由于提交被应用在其原始上下文之外,冲突比普通合并更常见。

当冲突发生时,Git 会暂停 cherry-pick 并标记冲突文件。你有三个选择:

1. 解决并继续:修复受影响文件中的冲突标记,用 git add 曲存已解决的文件,然后运行 git cherry-pick --continue。

解决并继续:修复受影响文件中的冲突标记,用 git add 曲存已解决的文件,然后运行 git cherry-pick --continue。

# Cherry-pick triggers a conflict
git cherry-pick abc1234
# CONFLICT (content): Merge conflict in src/auth.js

# 1. Open the file and resolve conflict markers
#    <<<<<<< HEAD
#    const timeout = 5000;
#    =======
#    const timeout = 3000;
#    >>>>>>> abc1234

# 2. Stage the resolved file
git add src/auth.js

# 3. Continue the cherry-pick
git cherry-pick --continue

2. 中止:运行 git cherry-pick --abort 完全取消 cherry-pick,回到开始前的状态。

中止:运行 git cherry-pick --abort 完全取消 cherry-pick,回到开始前的状态。

# Abort the cherry-pick and return to the previous state
git cherry-pick --abort

3. 跳过:运行 git cherry-pick --skip 跳过当前提交并继续下一个(当 cherry-pick 一个范围时)。

跳过:运行 git cherry-pick --skip 跳过当前提交并继续下一个(当 cherry-pick 一个范围时)。

# Skip the current conflicting commit in a range
git cherry-pick --skip

Git Revert:安全地撤销提交

git revert 创建一个新提交来撤销之前提交引入的变更。与 reset 不同,revert 不会重写历史——它是添加历史。这使得 revert 对于共享分支是安全的,因为其他协作者的历史不会被破坏。

# Revert the last commit
git revert HEAD

# Revert a specific commit
git revert abc1234

# Revert without auto-committing (stage changes only)
git revert --no-commit abc1234

# Revert multiple commits (creates one revert per commit)
git revert HEAD~3..HEAD

# Revert with a custom message
git revert abc1234 -m 1 --edit

撤销合并提交

撤销合并提交是一个特殊情况。合并提交有两个父提交,所以你必须使用 -m 标志告诉 Git 要回退到哪个父提交。在大多数情况下,-m 1 指定主线(你合并到的分支),这通常是你想要的。

# Revert a merge commit (keep the mainline, undo the merged branch)
git revert -m 1 <merge-commit-hash>

# -m 1 = keep parent 1 (the branch you merged INTO, e.g., main)
# -m 2 = keep parent 2 (the branch that was merged, e.g., feature)

# Example workflow:
git log --oneline --graph
# *   e5f6g7h (HEAD -> main) Merge branch 'feature-x'
# |\
# | * c3d4e5f Add feature X
# | * a1b2c3d Start feature X
# |/
# * 9z8y7x6 Previous main commit

# Undo the merge but keep main's history:
git revert -m 1 e5f6g7h

重要提示:撤销合并后,如果你以后想重新合并同一分支,Git 会认为这些变更已经存在。你需要先“撤销撤销”。

# If you need to re-merge feature-x later:
# First, revert the revert
git revert <revert-commit-hash>
# Then merge again
git merge feature-x

Git Reset:移动分支指针

git reset 将当前分支指针移动到指定的提交。根据使用的模式,它还可能修改曲存区(索引)和工作目录。理解三种模式对于安全使用 reset 至关重要。

--soft:仅移动 HEAD

将 HEAD 移动到目标提交。曲存区和工作目录不变。所有“移除”提交的变更会显示为已曲存(准备提交)。非常适合压缩提交或重新编写提交消息。

# Undo the last commit, keep everything staged
git reset --soft HEAD~1

# Undo the last 3 commits, keep all changes staged
git reset --soft HEAD~3

# Use case: squash last 3 commits into one
git reset --soft HEAD~3
git commit -m "Combined: feature implementation"

--mixed(默认):移动 HEAD + 取消曲存

将 HEAD 移动到目标提交并重置曲存区。变更保留在工作目录中作为未曲存的修改。这是不带标志运行 git reset 时的默认模式。

# Undo last commit, unstage changes (default mode)
git reset HEAD~1
# same as:
git reset --mixed HEAD~1

# Unstage a specific file without changing it
git reset HEAD src/app.js
# modern alternative:
git restore --staged src/app.js

--hard:移动 HEAD + 取消曲存 + 丢弃变更

将 HEAD 移动到目标提交,重置曲存区,并丢弃工作目录中的所有变更。这是破坏性的——未提交的工作将永久丢失(除非已提交的变更可通过 reflog 恢复)。

# ⚠️ DESTRUCTIVE: Undo last commit AND discard all changes
git reset --hard HEAD~1

# ⚠️ DESTRUCTIVE: Reset to match remote branch exactly
git reset --hard origin/main

# ⚠️ DESTRUCTIVE: Discard all uncommitted changes
git reset --hard HEAD

对比表:git reset 模式

模式HEAD曲存区(索引)工作目录安全?
--soft移动不变不变
--mixed移动重置不变
--hard移动重置重置(删除)
# Visual diagram of git reset modes:
#
# Commit History:  A --- B --- C --- D (HEAD)
#
# git reset --soft B:
#   HEAD -> B,  staging = C+D changes,  working dir = C+D changes
#
# git reset --mixed B:
#   HEAD -> B,  staging = clean,  working dir = C+D changes
#
# git reset --hard B:
#   HEAD -> B,  staging = clean,  working dir = clean (C+D GONE)

Reset vs Revert vs Checkout:何时使用哪个

这三个命令都可以“撤销”变更,但它们在根本上在不同层面工作。选择正确的命令取决于提交是否已推送以及你是否想保留历史。

  • git reset: git reset:通过向后移动分支指针重写历史。仅用于本地/未推送的提交。不能安全地用于共享分支。
  • git revert: git revert:创建一个新提交来反转变更。对共享分支安全。不重写历史。当提交已被推送时使用。
  • git checkout / restore: git checkout(或 git restore):修改工作目录而不影响提交或分支。用于丢弃特定文件的未提交变更。

黄金法则:如果提交已被推送到共享远程仓库,使用 revert。如果仅在本地,reset 更干净。

# Decision flowchart:
#
# Is the commit pushed to a shared remote?
# ├── YES → git revert <commit>
# └── NO
#     ├── Want to remove commit entirely? → git reset --hard HEAD~1
#     ├── Want to redo commit?            → git reset --soft HEAD~1
#     └── Want to discard file changes?   → git restore <file>

撤销 Cherry-Pick

如果你 cherry-pick 了一个提交并想要撤销它,根据是否已推送有两种方法:

未推送的情况

未推送:使用 git reset 将 HEAD 移回 cherry-pick 提交之前。这会干净地从历史中移除它。

# Undo the cherry-pick (it was the last commit)
git reset --hard HEAD~1

# Or if you want to keep the changes for review:
git reset --soft HEAD~1

已推送的情况

已推送:对 cherry-pick 的提交使用 git revert。这会创建一个新提交来反转 cherry-pick,而不重写共享历史。

# Find the cherry-picked commit hash
git log --oneline -5

# Revert it (safe for shared branches)
git revert <cherry-picked-commit-hash>
git push

跨仓库 Cherry-Pick

有时你需要从不同的仓库 cherry-pick 一个提交(例如从上游项目或同事的 fork)。Git 通过添加其他仓库为远程、获取其提交、然后 cherry-pick 来支持这一操作。

工作流程包含三个步骤:添加远程、获取其分支、然后按哈希值 cherry-pick 特定提交。

# Step 1: Add the other repository as a remote
git remote add upstream https://github.com/original/project.git

# Step 2: Fetch commits from the remote
git fetch upstream

# Step 3: Cherry-pick the specific commit
git cherry-pick <commit-hash-from-upstream>

# Full example: cherry-pick a fix from a colleague's fork
git remote add alice https://github.com/alice/project.git
git fetch alice
git log alice/main --oneline -10   # find the commit hash
git cherry-pick abc1234             # apply it

# Clean up: remove the remote when done
git remote remove alice

交互式 Rebase 作为 Cherry-Pick 的替代方案

在很多情况下,交互式 rebase(git rebase -i)可以完成 cherry-pick 做的事情,但更灵活。当你想在同一分支内重新排序、压缩、编辑或删除提交时,Rebase 更好。当你想在不同分支之间复制提交时,cherry-pick 更好。

何时用 rebase 代替 cherry-pick:

  • 你想在合并功能分支前清理提交
  • 你需要在同一分支内重新排序提交
  • 你想将多个提交压缩为一个
  • 你要移动整个提交序列(不仅仅是一两个)
# Interactive rebase: reorder, squash, edit, or drop commits
git rebase -i HEAD~5

# Opens an editor with:
# pick a1b2c3d First commit
# pick d4e5f6g Second commit
# pick h7i8j9k Third commit
# pick l0m1n2o Fourth commit
# pick p3q4r5s Fifth commit
#
# Change "pick" to:
#   squash (s) - combine with previous commit
#   edit (e)   - pause to amend this commit
#   drop (d)   - remove this commit
#   reword (r) - change commit message

何时用 cherry-pick 代替 rebase:

  • 你需要将特定提交复制到完全不同的分支
  • 你想将修复回移到发布/维护分支
  • 你只需要一两个提交,不是整个序列
# Cherry-pick is better for cross-branch operations:
git checkout release/2.0
git cherry-pick develop~3      # specific fix from develop
git cherry-pick -x abc1234     # with source tracking

真实场景

让我们来看看最常见的真实场景,其中 cherry-pick、revert 和 reset 是正确的工具。

场景 1:热修复回移

生产环境发现了一个严重 bug。修复已经在 develop 分支上提交。你需要将这个修复应用到发布分支,而不拉取未完成的功能。

# 1. Find the fix commit on develop
git log develop --oneline --grep="fix critical"
# abc1234 Fix critical auth bypass vulnerability

# 2. Switch to the release branch
git checkout release/1.0

# 3. Cherry-pick the fix with source tracking
git cherry-pick -x abc1234

# 4. Push to the release branch
git push origin release/1.0

# 5. Tag the hotfix release
git tag -a v1.0.1 -m "Hotfix: auth bypass"
git push origin v1.0.1

场景 2:功能提取

一个开发者在一个较大的功能分支中提交了一个有用的工具函数。另一个团队立即需要这个工具,但无法等待完整功能合并。

# 1. Find the utility commit in the feature branch
git log feature/big-refactor --oneline
# ghi9012 Add date formatting utility
# def5678 Refactor user module (WIP)
# abc1234 Start big refactor

# 2. Cherry-pick just the utility commit
git checkout main
git cherry-pick ghi9012

# 3. Push so the other team can use it
git push origin main

场景 3:发布分支补丁

你维护多个发布分支(v1.x、v2.x)。在 main 上提交的安全修复需要应用到所有活跃的发布分支。

# Security fix committed on main
git log main --oneline -1
# abc1234 Fix: patch XSS vulnerability in input sanitizer

# Apply to all active release branches
git checkout release/1.x
git cherry-pick -x abc1234
git push origin release/1.x

git checkout release/2.x
git cherry-pick -x abc1234
git push origin release/2.x

# Verify the fix is on all branches
git log release/1.x --oneline -1
git log release/2.x --oneline -1

危险操作和安全网

一些 Git 操作如果不小心使用会导致永久数据丢失。理解风险并知道恢复机制至关重要。

git reset --hard

git reset --hard:永久丢弃未提交的变更。此操作后无法恢复未曲存或未跟踪的文件。始终在使用 --hard 前检查 git status 和 git stash。

# ⚠️ Before using --hard, always check:
git status          # any uncommitted changes?
git stash           # save them first if needed

# If you already ran reset --hard and need to recover:
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: Important work
# ghi9012 HEAD@{2}: commit: More important work

# Recover by resetting to the pre-reset state:
git reset --hard def5678
# Or create a recovery branch:
git branch recovery def5678

git push --force

git push --force:覆盖远程分支历史。如果其他开发者已基于旧历史提交,这可能破坏他们的工作。优先使用 --force-with-lease,它会在远程已更新时拒绝推送。

# ⚠️ NEVER force push to shared branches without coordination
# BAD: Overwrites remote history unconditionally
git push --force

# BETTER: Only push if remote hasn't changed since last fetch
git push --force-with-lease

# SAFEST: Fetch first, then force-with-lease
git fetch origin
git push --force-with-lease origin feature-branch

git reflog (安全网)

安全网——git reflog:Git 在 reflog 中记录每次 HEAD 移动。即使在破坏性 reset 后,你通常可以通过在 reflog 中找到提交哈希并签出或从中创建分支来恢复提交。reflog 默认保留 90 天。

# View the reflog (all HEAD movements)
git reflog

# Example output:
# abc1234 HEAD@{0}: reset: moving to origin/main
# def5678 HEAD@{1}: commit: Add new feature
# ghi9012 HEAD@{2}: cherry-pick: Fix auth bug
# jkl3456 HEAD@{3}: checkout: moving from feature to main

# Recover ANY previous state:
git checkout def5678          # detached HEAD at that commit
git branch recovery def5678   # or create a branch

# The reflog is kept for 90 days by default
# Check the expiry setting:
git config gc.reflogExpire

常见问题

git cherry-pick 和 git merge 有什么区别?

git merge 将一个分支的所有提交集成到另一个分支,创建一个连接两个历史的合并提交。git cherry-pick 只复制特定的单个提交,并将它们作为新提交应用到当前分支。想集成整个分支时用 merge;只需要特定提交时用 cherry-pick。

可以 cherry-pick 一个合并提交吗?

可以,但你必须使用 -m 标志指定使用哪个父提交。例如,"git cherry-pick -m 1 <合并提交哈希>" 告诉 Git 使用第一个父提交(被合并到的分支)作为基础。但是,cherry-pick 合并提交通常不被推荐,因为它可能导致混乱的重复变更。考虑改为 cherry-pick 被合并分支中的各个提交。

如何撤销 git reset --hard?

如果你用 reset --hard 丢失了提交,使用 "git reflog" 找到 reset 之前的提交哈希,然后运行 "git reset --hard <哈希>" 或 "git branch recovery <哈希>" 来恢复。但是,未提交的变更(从未曲存或提交的文件)在硬重置后无法被 Git 恢复。

应该使用 git reset 还是 git revert 来撤销提交?

对本地/未推送的提交使用 git reset——它通过完全移除提交提供更干净的历史。对已推送到共享远程的提交使用 git revert——它创建一个新的“撤销”提交而不重写历史。如果不确定其他人是否已拉取你的提交,始终使用 revert 以确保安全。

为什么我的 cherry-pick 创建了与原始不同的提交哈希?

提交哈希由提交内容、父提交、作者日期和提交者日期决定。当你 cherry-pick 时,新提交有不同的父提交和新的提交者日期,所以即使代码变更相同,也会得到不同的哈希。这是设计如此的——Git 将它们视为不同的提交。使用 -x 标志在消息中添加对原始提交的引用。

掌握 git cherry-pick、revert 和 reset 让你能精确控制仓库历史。记住:本地清理用 reset,共享分支用 revert,复制特定提交用 cherry-pick。如有疑问,检查 git reflog——它是你的安全网。

试试 Git 命令生成器 →

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

🔀Git Command Generator.gi.gitignore Generator±Text Diff Checker

相关文章

Git 撤销最后一次提交:5 种保留更改的修复方法

学习五种撤销最后一次 git 提交并保留更改的方法。涵盖 git reset --soft、git revert、git amend、交互式 rebase 和 reflog 恢复。

Git Rebase vs Merge:何时使用哪个(图解对比)

理解 git rebase 和 merge 的区别。学习何时使用哪个,避免常见陷阱,掌握 Git 工作流。