如何撤销 Git cherry-pick 操作

GitBeginner
立即练习

介绍

Git 的 cherry-pick 功能允许你将特定提交从一个分支应用到另一个分支。虽然功能强大,但有时你可能需要由于错误或冲突而撤销 cherry-pick 操作。在这个实验(Lab)中,你将学习如何执行 cherry-pick,然后在必要时使用各种方法来撤销它。到最后,你将拥有 cherry-pick 操作的实践经验,以及从常见问题中恢复的技能。

设置测试仓库

在这一步,我们将创建一个测试 Git 仓库来练习 cherry-pick 操作。这将提供一个安全的环境来试验各种 Git 命令。

创建一个新的 Git 仓库

让我们从为我们的测试仓库创建一个新目录,并将其初始化为 Git 仓库开始:

mkdir -p ~/project/cherry-pick-lab
cd ~/project/cherry-pick-lab
git init

你应该看到类似这样的输出:

Initialized empty Git repository in /home/labex/project/cherry-pick-lab/.git/

设置 Git 用户配置

在我们可以进行提交之前,我们需要为 Git 设置用户名和电子邮件:

git config --local user.name "LabEx User"
git config --local user.email "labex@example.com"

在主分支上创建初始提交

让我们在主分支上创建一些初始提交:

## Create and commit the first file
echo "## Cherry Pick Lab" > README.md
git add README.md
git commit -m "Initial commit with README"

## Create and commit a second file
echo "console.log('Hello, world!');" > app.js
git add app.js
git commit -m "Add main application file"

查看提交历史

让我们检查一下我们的提交历史,以确保一切设置正确:

git log --oneline

你应该看到类似这样的输出:

abcd123 (HEAD -> main) Add main application file
efgh456 Initial commit with README

实际的提交哈希值在你的系统上会有所不同。记下 "Add main application file" 的提交哈希值,我们稍后会用到它。

创建一个功能分支

现在,让我们创建一个功能分支,在那里我们将进行一些额外的更改:

git checkout -b feature-branch

你应该看到类似这样的输出:

Switched to a new branch 'feature-branch'

现在让我们向这个功能分支添加一些提交:

## Create and commit a new feature file
echo "function newFeature() { return 'awesome'; }" > feature.js
git add feature.js
git commit -m "Add new feature function"

## Modify the README file
echo -e "## Cherry Pick Lab\n\nThis repo demonstrates git cherry-pick operations." > README.md
git add README.md
git commit -m "Update README with project description"

现在我们有一个包含两个提交的主分支和一个包含两个额外提交的功能分支。在下一步中,我们将使用 cherry-pick 将其中一个功能分支提交应用到主分支。

执行 Cherry-pick 操作

在这一步,我们将学习如何使用 cherry-pick 命令将特定提交从一个分支应用到另一个分支。当你想要有选择地将更改从一个分支合并到另一个分支时,这非常有用。

理解 Cherry-pick

Git 中的 cherry-picking 允许你从一个分支中选取一个特定的提交,并将其应用到另一个分支。与通常应用多个提交的合并或变基(rebasing)不同,cherry-picking 一次只应用一个提交。

切换回主分支

首先,让我们切换回主分支,在那里我们想要应用来自功能分支的提交:

git checkout main

你应该看到确认切换的输出:

Switched to branch 'main'

查看功能分支提交

在 cherry-picking 之前,让我们检查一下功能分支中我们可能想要应用到主分支的提交:

git log feature-branch --oneline

这将显示功能分支中的所有提交,包括与主分支共享的提交。你将看到类似这样的输出:

1234abc Update README with project description
5678def Add new feature function
abcd123 Add main application file
efgh456 Initial commit with README

记下 "Add new feature function" 的提交哈希值(在这个例子中,它是 5678def)。我们将在下一步中使用这个哈希值。

Cherry-picking 一个提交

现在让我们将 "Add new feature function" 提交从功能分支 cherry-pick 到我们的主分支中:

git cherry-pick [COMMIT_HASH]

[COMMIT_HASH] 替换为你之前记下的实际哈希值。例如:

git cherry-pick 5678def

如果 cherry-pick 成功,你将看到类似这样的输出:

[main 98765ab] Add new feature function
 1 file changed, 1 insertion(+)
 create mode 100644 feature.js

验证 Cherry-pick

让我们确认 cherry-pick 按预期工作:

git log --oneline

你现在应该在你的主分支的历史记录中看到 cherry-picked 的提交:

98765ab (HEAD -> main) Add new feature function
abcd123 Add main application file
efgh456 Initial commit with README

你也可以验证该文件是否存在:

ls -la

你应该在输出中看到 feature.js

total 16
drwxr-xr-x 3 labex labex 4096 Jan 1 00:00 .
drwxr-xr-x 3 labex labex 4096 Jan 1 00:00 ..
drwxr-xr-x 8 labex labex 4096 Jan 1 00:00 .git
-rw-r--r-- 1 labex labex   29 Jan 1 00:00 app.js
-rw-r--r-- 1 labex labex   42 Jan 1 00:00 feature.js
-rw-r--r-- 1 labex labex   16 Jan 1 00:00 README.md

cherry-pick 操作已成功将提交从功能分支应用到主分支。在下一步中,我们将学习如何在需要时撤销这个 cherry-pick。

使用 Git Reset 撤销 Cherry-pick

现在我们已经成功地 cherry-pick 了一个提交,让我们学习如何撤销此操作。在这一步,我们将使用 git reset 命令,这是撤销最近的 cherry-pick 的最直接方法。

理解 Git Reset

git reset 命令将当前分支指针移动到指定的提交,有效地“撤销”该点之后出现的任何提交。git reset 有三种主要模式:

  • --soft:移动分支指针,但保留已暂存的更改
  • --mixed(默认):移动分支指针并取消暂存更改
  • --hard:移动分支指针并丢弃所有更改

为了撤销 cherry-pick,我们将使用 --hard 选项来完全删除 cherry-picked 的提交及其更改。

检查当前状态

首先,让我们检查一下当前状态,以确认我们位于包含 cherry-picked 提交的主分支上:

git log --oneline -n 3

你应该看到类似这样的输出:

98765ab (HEAD -> main) Add new feature function
abcd123 Add main application file
efgh456 Initial commit with README

使用 Git Reset 撤销 Cherry-pick

要撤销 cherry-pick 操作,我们将使用带有 --hard 选项的 git reset,将分支指针移回一个提交:

git reset --hard HEAD~1

此命令告诉 Git 将分支重置到当前 HEAD 之前的提交(~1 部分表示“前一个提交”)。

你应该看到类似这样的输出:

HEAD is now at abcd123 Add main application file

验证重置

让我们验证一下 cherry-pick 是否已被撤销:

git log --oneline

你应该看到 cherry-picked 的提交不再出现在历史记录中:

abcd123 (HEAD -> main) Add main application file
efgh456 Initial commit with README

我们还检查一下 feature.js 文件是否已被删除:

ls -la

输出不应包含 feature.js

total 12
drwxr-xr-x 3 labex labex 4096 Jan 1 00:00 .
drwxr-xr-x 3 labex labex 4096 Jan 1 00:00 ..
drwxr-xr-x 8 labex labex 4096 Jan 1 00:00 .git
-rw-r--r-- 1 labex labex   29 Jan 1 00:00 app.js
-rw-r--r-- 1 labex labex   16 Jan 1 00:00 README.md

恭喜你!你已经使用 git reset 成功地撤销了 cherry-pick 操作。这种方法干净而简单,但它会重写历史记录,因此它应该仅用于尚未推送到共享仓库的本地更改。

使用 Git Revert 撤销 Cherry-pick

在之前的步骤中,我们使用了 git reset 来撤销 cherry-pick。但是,git reset 会重写历史记录,如果你已经将更改推送到共享仓库,这可能会有问题。在这一步中,我们将学习如何使用 git revert 来安全地撤销 cherry-pick,而无需重写历史记录。

理解 Git Revert

git revert 命令创建一个新的提交,该提交会撤销先前提交引入的更改。与从历史记录中删除提交的 git reset 不同,git revert 添加一个新的提交来抵消更改,从而保留提交历史记录。

再次执行 Cherry-pick

首先,让我们再次 cherry-pick 提交,以便我们有一些东西可以 revert:

## Get the commit hash from the feature branch
FEATURE_HASH=$(git log feature-branch --oneline | grep "new feature" | cut -d ' ' -f 1)

## Cherry-pick the commit
git cherry-pick $FEATURE_HASH

你应该看到类似这样的输出:

[main 98765ab] Add new feature function
 1 file changed, 1 insertion(+)
 create mode 100644 feature.js

检查当前状态

让我们确认 cherry-pick 成功了:

git log --oneline -n 3
ls -la

你应该在历史记录中看到 cherry-picked 的提交,并在目录列表中看到 feature.js 文件。

Reverting Cherry-pick

现在,让我们使用 git revert 来撤销 cherry-pick,同时保留历史记录:

git revert HEAD --no-edit

--no-edit 标志告诉 Git 使用默认的提交消息,而无需打开编辑器。

你应该看到类似这样的输出:

[main abc9876] Revert "Add new feature function"
 1 file changed, 1 deletion(-)
 delete mode 100644 feature.js

验证 Revert

让我们检查一下提交历史记录:

git log --oneline -n 4

你应该同时看到 cherry-picked 的提交和 revert 提交:

abc9876 (HEAD -> main) Revert "Add new feature function"
98765ab Add new feature function
abcd123 Add main application file
efgh456 Initial commit with README

现在,让我们检查一下 feature.js 文件是否已被删除:

ls -la

输出不应包含 feature.js

total 12
drwxr-xr-x 3 labex labex 4096 Jan 1 00:00 .
drwxr-xr-x 3 labex labex 4096 Jan 1 00:00 ..
drwxr-xr-x 8 labex labex 4096 Jan 1 00:00 .git
-rw-r--r-- 1 labex labex   29 Jan 1 00:00 app.js
-rw-r--r-- 1 labex labex   16 Jan 1 00:00 README.md

虽然 git resetgit revert 都实现了相同的最终结果(删除 cherry-pick 引入的更改),但它们的方式不同:

  • git reset 从历史记录中删除提交,如果提交已与其他人共享,这可能会导致问题。
  • git revert 添加一个新的提交来撤销更改,从而保留提交历史记录,这对于共享仓库来说更安全。

在这些方法之间进行选择取决于你的具体情况:

  • 对尚未共享的本地更改使用 git reset
  • 对已推送到共享仓库的更改使用 git revert

处理 Cherry-pick 冲突

有时,当你 cherry-pick 一个提交时,如果提交中的更改与你当前分支中的更改冲突,Git 可能会遇到冲突。在这一步中,我们将学习如何处理 cherry-pick 冲突以及如何中止 cherry-pick 操作。

创建一个具有潜在冲突的场景

首先,让我们创建一个将导致 cherry-pick 冲突的场景:

## Switch to main branch and modify README.md
git checkout main
echo -e "## Cherry Pick Lab\n\nThis is the main branch README." > README.md
git commit -am "Update README in main branch"

## Switch to feature branch and make a conflicting change to README.md
git checkout feature-branch
echo -e "## Cherry Pick Lab\n\nThis README has been updated in the feature branch." > README.md
git commit -am "Update README in feature branch"

尝试带有冲突的 Cherry-pick

现在,让我们切换回主分支并尝试从功能分支 cherry-pick 提交:

git checkout main
CONFLICT_HASH=$(git log feature-branch --oneline | grep "Update README in feature" | cut -d ' ' -f 1)
git cherry-pick $CONFLICT_HASH

由于两个分支都修改了 README.md 中的相同行,你应该会看到一个冲突:

error: could not apply a1b2c3d... Update README in feature branch
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

查看冲突

让我们检查一下冲突:

git status

你应该看到输出表明 README.md 中存在冲突:

On branch main
You are currently cherry-picking commit a1b2c3d.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
  both modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")

让我们看一下冲突文件的内容:

cat README.md

你应该看到类似这样的内容:

## Cherry Pick Lab

<<<<<<< HEAD
This is the main branch README.
=======
This README has been updated in the feature branch.
>>>>>>> a1b2c3d... Update README in feature branch

解决冲突

要解决冲突,我们需要编辑文件并决定保留哪些更改。让我们修改 README.md 以包含这两个更改:

echo -e "## Cherry Pick Lab\n\nThis is the main branch README.\n\nThis README has also been updated with content from the feature branch." > README.md

现在,让我们将冲突标记为已解决并继续 cherry-pick:

git add README.md
git cherry-pick --continue

Git 将打开一个带有默认提交消息的编辑器。保存并关闭编辑器以完成 cherry-pick。

中止 Cherry-pick

有时,你可能决定不想解决冲突,而更愿意取消 cherry-pick 操作。让我们创建另一个冲突,然后中止 cherry-pick:

## Create another conflicting commit in feature branch
git checkout feature-branch
echo "// This will conflict with app.js in main" > app.js
git commit -am "Modify app.js in feature branch"

## Try to cherry-pick this commit to main
git checkout main
ANOTHER_CONFLICT=$(git log feature-branch --oneline | grep "Modify app.js" | cut -d ' ' -f 1)
git cherry-pick $ANOTHER_CONFLICT

你应该看到另一个冲突。这次,让我们中止 cherry-pick:

git cherry-pick --abort

你应该看到 cherry-pick 操作已被取消,并且你的工作目录已恢复到之前的状态:

git status

输出:

On branch main
nothing to commit, working tree clean

在 cherry-pick 操作期间处理冲突是 Git 用户的一项基本技能。当你遇到冲突时,你有三个选项:

  1. 手动解决冲突,然后使用 git addgit cherry-pick --continue
  2. 使用 git cherry-pick --skip 跳过冲突的提交
  3. 使用 git cherry-pick --abort 中止整个 cherry-pick 操作

最佳方法取决于具体情况和你的项目需求。

总结

在这个实验中,你获得了使用 Git 的 cherry-pick 功能的实践经验,并学习了多种撤销 cherry-pick 操作的方法:

  1. 你创建了一个测试仓库,其中包含多个分支和提交,以便进行练习
  2. 你执行了 cherry-pick 操作,将一个特定提交从一个分支应用到另一个分支
  3. 你学习了如何使用 git reset 撤销 cherry-pick,这适用于本地更改
  4. 你探索了如何使用 git revert 安全地撤销 cherry-pick,这保留了历史记录
  5. 你练习了处理 cherry-pick 冲突,并学习了如何中止 cherry-pick 操作

这些技能在实际项目中使用 Git 时非常宝贵。Cherry-picking 允许你选择性地跨分支应用更改,而了解如何撤销 cherry-pick 可以帮助你从错误中恢复并保持干净的 Git 历史记录。

请记住,不同的撤销方法服务于不同的目的:

  • 对尚未共享的本地更改使用 git reset
  • 对已推送到共享仓库的更改使用 git revert
  • 使用 git cherry-pick --abort 取消正在进行的 cherry-pick 操作

通过理解这些选项,你可以为你的特定情况选择最合适的方法。