当前位置: 首页 > news >正文

[Git] 如何进行版本回退

版本控制系统最重要的能力之一,就是能够轻松地在项目的不同历史版本之间切换。有时,你可能发现最近的修改引入了严重问题,或者需要回到之前的某个节点重新开始。这时,“版本回退”功能就派上用场了。

版本回退:反方向的钟~~

Git 提供了强大的版本回退(或称为“重置”)功能,让你能够将项目状态恢复到历史上的任意一个提交点。执行版本回退的命令是 git reset

要理解 git reset,关键在于认识到它主要做了两件事(或者说,你可以控制它做哪几件事):

  1. 移动分支指针: Git 的版本历史是一个由 Commit 对象组成的链条,每个 Commit 对象都有一个唯一的 ID。分支(比如 mastermain)本质上只是一个指向最新 Commit 对象的指针。git reset 命令首先会让你选择一个历史的 Commit 对象,然后把当前分支的指针移动到你指定的那个 Commit 对象上。这样一来,从这个 Commit 之后的版本就不再是当前分支的“历史”了(至少暂时是这样)。
  2. 重置暂存区和工作区(可选): 在移动分支指针之后,git reset 还可以根据你指定的选项,进一步修改暂存区工作区的内容,让它们也回退到目标 Commit 时的状态。

git reset 命令的基本语法是:

git reset [--soft | --mixed | --hard] [目标版本]

这里有几个重要的部分需要解释:

  • [目标版本] 你想回退到哪个历史版本?你可以用以下方式指定:
    • 完整的 Commit ID 或部分 ID: 最精确的方式。你可以从 git loggit reflog 里复制某个提交的完整 ID,或者只需要足够区分该提交的前几位 ID 即可(通常 7-8 位就够了)。
    • HEAD 表示当前分支最新的一次提交(也就是你当前所处的版本)。git reset HEAD 实际上是撤销 git add 操作,将暂存区的改动移回工作区(这是 --mixed 模式下的默认行为)。
    • HEAD^ 表示当前版本的上一个版本。一个 ^ 表示往前回退一级。
    • HEAD^^ 表示上上个版本。
    • HEAD~数字~ 加上数字表示往前回退多少个版本。例如 HEAD~1 是上一个版本,HEAD~2 是上上个版本,HEAD~0 是当前版本。这在回退多个版本时比用 ^ 更方便。
  • [--soft | --mixed | --hard] 这是决定回退后,暂存区工作区状态的关键参数。
    • --soft
      • 版本库: 回退到指定的历史版本(移动分支指针和 HEAD)。
      • 暂存区: 不变。保留回退前暂存区的内容。
      • 工作区: 不变。保留回退前工作区的内容。
      • 效果: 相当于撤销了回退目标版本之后的所有 commit 操作,但保留了这些修改在暂存区和工作区。你可以重新 commit 这些改动(比如合并提交或修改提交信息)。
    • --mixed** (默认选项):**
      • 版本库: 回退到指定的历史版本(移动分支指针和 HEAD)。
      • 暂存区: 重置为目标版本时的状态。也就是说,回退目标版本之后的改动会从暂存区中移除。
      • 工作区: 不变。保留回退前工作区的内容。
      • 效果: 撤销了回退目标版本之后的所有 commit 操作,并清空了暂存区。回退目标版本之后的所有改动都会回到工作区,成为未暂存(unstaged)的状态。这是最常用的模式,适合想撤销提交,但又想保留代码改动、重新组织提交的场景。git reset [目标版本] (不带参数)默认就是 --mixed
    • --hard
      • 版本库: 回退到指定的历史版本(移动分支指针和 HEAD)。
      • 暂存区: 重置为目标版本时的状态。
      • 工作区: 重置为目标版本时的状态。
      • 效果: 这是一个非常彻底的回退!它会丢弃回退目标版本之后的所有暂存区和工作区的改动。就像你的项目状态真的坐上了“时光机”,完全回到了那个历史版本。【重要警告】:使用 --hard 参数时要非常非常慎重!如果你的工作区有未提交的修改,git reset --hard永久丢弃这些修改,你将找不回来!请务必确认你不再需要这些改动,或者已经备份。

演示版本回退:从 version3 回到 version2

为了方便演示回退功能,我们先按照提供的例子,在 ReadMe 文件中添加内容并连续提交三个版本:

# 假设这是你的 gitcode 仓库
zz@139-159-150-152:~/gitcode$ pwd
/home/zz/gitcode# 第一个版本内容并提交
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
zz@139-159-150-152:~/gitcode$ git add ReadMe
zz@139-159-150-152:~/gitcode$ git commit -m"add version1"
[master cff9d1e] add version11 file changed, 1 insertion(+)# 第二个版本内容并提交
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
zz@139-159-150-152:~/gitcode$ git add ReadMe
zz@139-159-150-152:~/gitcode$ git commit -m"add version2"
[master 14c12c3] add version2 # 注意这里的 commit id 是 14c12c3...1 file changed, 1 insertion(+)# 第三个版本内容并提交 (当前最新版本)
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
hello version3
zz@139-159-150-152:~/gitcode$ git add ReadMe
zz@139-159-150-152:~/gitcode$ git commit -m"add version3"
[master d95c13f] add version3 # 注意这里的 commit id 是 d95c13f...1 file changed, 1 insertion(+)# 查看一下提交历史,确认有这三个版本
zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 (HEAD -> master) add version3 # 最新,HEAD 和 master 指向它
14c12c32464d6ead7159f5c24e786ce450c899dd add version2 # 上一个版本
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1 # 再上一个版本
... # 可能还有之前的其他提交

现在我们的仓库历史是:初始提交 -> version1 -> version2 -> version3 (当前)。HEAD 指针和 master 分支都指向 d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 这个 Commit ID。

假设我们发现 version3 的内容有问题,想完全回到 version2 的状态,并且工作区的文件内容也要变回 version2 时期。这时就需要使用 --hard 参数。

version2 是当前版本 (HEAD) 的上一个版本,所以我们可以用 HEAD^ 来指代 version2 这个版本。

# 我们想回退到 HEAD 的上一个版本 (version2),并且彻底重置工作区和暂存区
zz@139-159-150-152:~/gitcode$ git reset --hard HEAD^
HEAD is now at 14c12c3 add version2 # Git 告诉你 HEAD (和 master) 现在指向了这个 commit

或者,你也可以直接使用 version2 的 Commit ID 来指定目标版本(从 git log 输出中找到 add version2 那一行的 ID):

# 回退到指定的 version2 的 commit id
# 替换成你自己的 version2 的 commit id
zz@139-159-150-152:~/gitcode$ git reset --hard 14c12c32464d6ead7159f5c24e786ce450c899dd
HEAD is now at 14c12c3 add version2

执行 git reset --hard 后,Git 会将当前分支指针和 HEAD 都移到目标版本 (version2),同时强行把暂存区和工作区的内容都替换成目标版本时的文件内容。

我们查看一下 ReadMe 文件的内容:

zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2

惊奇地发现,ReadMe 文件的内容已经回退到 version2 时刻的状态了!version3 中添加的 hello version3 这一行已经不见了。

再用 git log 查看提交历史:

zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
14c12c32464d6ead7159f5c24e786ce450c899dd (HEAD -> master) add version2 # 最新,HEAD 和 master 指向它
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1 # 上一个版本
... # 可能还有之前的其他提交

注意看,git log 显示的最新提交已经是 version2 了,那个 add version3 的提交仿佛从历史中“消失”了!这是因为当前分支 (master) 的指针已经移回到了 version2 对应的 Commit,从这个分支看过去,version3 不再是它的历史一部分。

这就是版本回退!通过移动分支指针,让你的项目回到了之前的某个状态。

哎呀,回退错了怎么办?找回“消失”的提交!

执行了 git reset --hard 回退版本后,你可能会遇到一个问题:如果我回退到 version2 后,又后悔了,想再回到 version3 怎么办?

当你使用 git log 查看时,version3 的那个提交 ID ( d95c13f...) 似乎不见了,因为当前分支不指向它了。运气好的话你能在终端的滚动记录里找到它,运气不好,你就可能觉得那个版本永远丢失了。

别怕!Git 是一个强大的工具,它不会轻易丢掉你的提交。虽然 git log 显示的是当前分支的历史,但 Git 在本地还悄悄地记录着你的每一次操作历史,包括 HEAD 指针曾经指向的位置变化。这个历史记录可以通过 git reflog 命令查看。

git reflog:你的操作“流水账”

git reflog 命令记录了你的仓库中 HEAD 的每一次移动,几乎所有的 Git 操作(如 commit, reset, merge, rebase 等)都会在这里留下记录。

zz@139-159-150-152:~/gitcode$ git reflog
14c12c3 (HEAD -> master) HEAD@{0}: reset: moving to 14c12c32464d6ead7159f5c24e786ce450c899dd # 最近一次操作:reset,移动到 version2
d95c13f HEAD@{1}: commit: add version3 # 上上次操作:commit version3
14c12c3 (HEAD -> master) HEAD@{2}: commit: add version2 # 再之前的操作:commit version2
cff9d1e HEAD@{3}: commit: add version1
94da695 HEAD@{4}: commit: add modify ReadMe file
23807c5 HEAD@{5}: commit: add 3 files
c614289 HEAD@{6}: commit (initial): commit my first file

看到了吗?git reflog 清晰地列出了我执行过的操作,以及每次操作后 HEAD 指向的 Commit ID。即使 git log 看不到了,在这里我仍然能找到 add version3 那个提交的 ID (d95c13f)!

使用 git reflog 找回版本

既然在 git reflog 里找到了 version3 的 Commit ID,我们就可以再次使用 git reset --hard 命令,指定这个 ID,跳回到 version3 了!

# 使用 git reflog 里找到的 version3 的 commit id 来回退
# 这里使用了部分 commit id (d95c13f),通常只要部分 id 足够唯一即可
zz@139-159-150-152:~/gitcode$ git reset --hard d95c13f
HEAD is now at d95c13f add version3 # Git 告诉你 HEAD (和 master) 又回到了 version3# 检查工作区,内容回到了 version3
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
hello version3# 检查 git log,分支指针也回到了 version3
zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 (HEAD -> master) add version3
14c12c32464d6ead7159f5c24e786ce450c899dd add version2
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1
94da6950d27e623c0368b22f1ffc4bff761b5b00 add modify ReadMe file
23807c536969cd886c4fb624b997ca575756eed6 add 3 files
c61428926f3853d3229e278113095f115c302405 commit my first file # 注意这里的初始提交 ID 和前面 log 可能有差异,以你的实际输出为准

成功了!我们又从 version2 跳回到了 version3

这个例子说明:版本回退(reset)本质上是移动 **HEAD** 指针和分支指针。Git 的所有历史版本(Commit 对象)都还在对象库里。**git log**** 查看的是当前分支能追溯到的历史,而 **git reflog** 记录的是你本地仓库 **HEAD** 指针移动过的所有位置**。只要你想回到的那个版本的 Commit ID 还在 git reflog 里,你就可以回得去。

为什么 Git 回退这么快?

Git 版本回退速度非常快,特别是与一些中心化的版本控制系统不同。这是因为 Git 回退时,通常只是简单地修改指针的指向(比如 refs/heads/master 文件里存储的 Commit ID),而不是去删除对象库里已有的 Commit 对象或文件内容对象。

想象一下版本历史是一条链子上的珠子,每个珠子是一个 Commit。分支指针(比如 master)和 HEAD 指针就像两个环,套在其中一个珠子上,表示“我现在在这里”。

当你执行 git reset 回退时,比如从 version3 回到 version2,Git 只是把那个环从 version3 那个珠子上取下来,套到 version2 的珠子上。version3 那个珠子还在链子上,只是暂时没有分支指针指向它了。

版本1 --- 版本2 --- 版本3 (HEAD, master)  <- reset --hard HEAD^版本1 --- 版本2 (HEAD, master)   版本3

(这与你提供的第二个图片概念一致,HEAD和master指针从version3移到了version2)

只有在 Git 执行垃圾回收时,那些没有任何指针(包括分支、标签、或其他引用,以及 reflog 的过期记录)指向的 Commit 对象和相关联的对象,才可能被清理掉。所以,即使你 reset --hard 了,在一段时间内,那个被“回退掉”的版本数据仍然存在于你的 .git/objects 目录中,git reflog 就是找到它们的救命稻草。

总结:谨慎使用 reset --hard

通过这部分的学习,我们掌握了 Git 版本回退的核心命令 git reset

  • git reset 主要通过移动分支指针和可选地修改暂存区工作区来回退版本。
  • --soft 只移动指针,保留暂存区和工作区。
  • --mixed (默认) 移动指针并重置暂存区,保留工作区。
  • --hard 移动指针,并彻底重置暂存区和工作区可能导致未提交的修改永久丢失,请务必谨慎使用!
  • 你可以使用 Commit ID、HEAD^HEAD~数字 等方式指定回退目标。
  • git log 查看的是当前分支的历史,而 git reflog 查看的是本地仓库 HEAD移动历史,它是回退后找回丢失提交的“后悔药”。

版本回退是一个强大的工具,可以帮助你修正错误的历史。熟练掌握 git reset 的不同模式及其对工作区、暂存区和版本库的影响,以及学会使用 git reflog 来找回提交,是安全使用 Git 的重要一环。

下一篇,我们将学习如何撤销工作区和暂存区的修改。

相关文章:

  • skywalking 10.2 源码编译
  • Groovy:Java 的简洁版
  • 2022 年 9 月青少年软编等考 C 语言八级真题解析
  • 安卓无障碍脚本开发全教程
  • 计算机网络中的单播、组播与广播
  • 41-牧场管理系统
  • 相向双指针 -- 灵神刷题
  • xdvipdfmx:fatal: File ended prematurely. No output PDF file written.
  • 【笔记】如何解决GitHub报错403
  • JAVA网络编程——socket套接字的介绍上(详细)
  • Python:从脚本语言到工业级应用的传奇进化
  • Vue.js教学第十四章:Vuex模块化,打造高效大型应用状态管理
  • 网络安全给数据工厂带来的挑战
  • 操作系统与底层安全
  • STM32 USART串口通信
  • Todesk 软件被锁定,不记得安全密码也进不去软件改不了问题解决
  • n 阶矩阵 A 可逆的充分必要条件是 ∣ A ∣ ≠ 0
  • 关于 Web 安全:4. 中间件 框架风险点分析
  • 危化品经营单位安全生产管理人员考试主要内容
  • 嵌入式Openharmony系统应用开发与实现方法
  • 网站经营内容/app推广是什么意思
  • 网站开发gif图太多耗资源吗/西安关键词网站排名
  • 做网站 挣广告联盟的佣金/搜索引擎优化自然排名的优点
  • 保定网站设计制作/seo免费视频教程
  • 郑州hi宝贝网站建设公司/专业的seo搜索引擎优化培训
  • 一个软件开发的流程/优化营商环境工作开展情况汇报