[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 的重要一环。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/82085.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

易贝平台关键字搜索技术深度解析

一、核心搜索机制 关键词匹配原理 采用TF-IDF算法计算关键词权重 支持同义词扩展&#xff08;如"phone"匹配"cellphone"&#xff09; 标题权重 > 副标题 > 商品描述 搜索排序因素 # 搜索权重模拟计算 def calculate_rank(keyword, item): title…

深度剖析 MCP SDK 最新版:Streamable HTTP 模式

好记忆不如烂笔头&#xff0c;能记下点东西&#xff0c;就记下点&#xff0c;有时间拿出来看看&#xff0c;也会发觉不一样的感受. 目录 一、概述 二、快速上手&#xff1a;开启 Streamable HTTP 服务端开启 客户端连接 三、深入两个核心参数 stateless_http json_resp…

树莓派开箱上手教程(无需显示器版)

树莓派开箱上手教程&#xff08;无需显示器版&#xff09; 硬件准备 名称参数电源适配器5V电源适配器&#xff0c;至少需要3A的额定电流&#xff0c;配备USB Type-C输出接头microSD卡用来将树莓派的操作系统安装到上边&#xff0c;至少需要8GB容量&#xff0c;一般建议16GB及以…

MySQL强化关键_015_存储过程

目 录 一、概述 1.说明 2.优点 3.缺点 二、存储过程的操作 1.创建 2.调用 3.查看 4.删除 三、变量 1.系统变量 &#xff08;1&#xff09;说明 &#xff08;2&#xff09;查看系统变量 &#xff08;3&#xff09;设置系统变量 2.用户变量 &#xff08;1&…

动态规划dp

这里写目录标题 动态规划01背包完全背包多重背包混合背包二维费用的背包分组背包有依赖的背包背包问题求方案数背包问题求具体方案数位 DP状压 DP常用例题 动态规划 01背包 有 n n n 件物品和一个容量为 W W W 的背包&#xff0c;第 i i i 件物品的体积为 w [ i ] w[i] w…

arcgis js统计FeatureLayer的椭球面积、平面面积

1、导入依赖 import FeatureLayer from arcgis/core/layers/FeatureLayer import { geodesicArea, planarArea, simplify } from arcgis/core/geometry/geometryEngine; import { project, load as projectionLoad } from arcgis/core/geometry/projection2、初始化project o…

2.2.1 05年T2

引言 本文将从一预习、二自习、三学习、四复习等四个阶段来分析2005年考研英语阅读第二篇文章。为了便于后续阅读&#xff0c;我将第四部分复习放在了首位。 四、复习 方法&#xff1a;错误思路分析总结考点文章梳理 4.1 错题分析 题目&#xff1a;26&#xff08;细节题&…

Java 连接并操作 Redis 万字详解:从 Jedis 直连到 RedisTemplate 封装,5 种方式全解析

引言 在分布式系统和高并发场景中&#xff0c;Redis 作为高性能内存数据库的地位举足轻重。对于 Java 开发者而言&#xff0c;掌握 Redis 的连接与操作是进阶必备技能。然而&#xff0c;从基础的 Jedis 原生客户端到 Spring 封装的 RedisTemplate&#xff0c;不同连接方式的原…

谈谈对《加密算法》的理解

文章目录 一、什么是加密算法&#xff1f;二、常见的加密算法有哪些&#xff1f;2.1 对称加密2.2 非对称加密2.3 哈希算法 三、加密算法代码展示3.1 MD5加密3.2 秘钥加密3.3 AES加密解密 四、加密算法的使用场景 一、什么是加密算法&#xff1f; 加密算法是一种通过数学方法将…

Fuzz 模糊测试篇JS 算法口令隐藏参数盲 Payload未知文件目录

1 、 Fuzz 是一种基于黑盒的自动化软件模糊测试技术 , 简单的说一种懒惰且暴力的技术融合了常见 的以及精心构建的数据文本进行网站、软件安全性测试。 2 、 Fuzz 的核心思想 : 口令 Fuzz( 弱口令 ) 目录 Fuzz( 漏洞点 ) 参数 Fuzz( 利用参数 ) PayloadFuzz(Bypass)…

哈希表的实现(下)

目录 前言 开散列概念 开散列实现 Insert 优化 Find Erase 前言 上一章节我们用闭散列实现了一下哈希表&#xff0c;但存在一些问题&#xff0c;比如空间浪费比较严重&#xff0c;如果连续一段空间都已经存放值&#xff0c;那么在此位置插入新值的时候就会一直挪动&…

再谈Linux 进程:进程等待、进程替换与环境变量

目录 1.进程等待 为什么需要进程等待&#xff1f; 相关系统调用&#xff1a;wait()和waitpid() wait(): waitpid(): 解析子进程状态&#xff08;status&#xff09; 2.进程替换 为什么需要进程替换&#xff1f; 相关系统调用&#xff1a;exec函数家族 3.环境变量 ​…

基于深度学习的无线电调制识别系统

基于深度学习的无线电调制识别系统 本项目实现了一个基于深度学习的无线电调制识别系统&#xff0c;使用LSTM&#xff08;长短期记忆网络&#xff09;模型对不同类型的 无线电信号进行自动分类识别。该系统能够在不同信噪比(SNR)条件下&#xff0c;准确识别多种调制类型&#…

Python 爬虫之requests 模块的应用

requests 是用 python 语言编写的一个开源的HTTP库&#xff0c;可以通过 requests 库编写 python 代码发送网络请求&#xff0c;其简单易用&#xff0c;是编写爬虫程序时必知必会的一个模块。 requests 模块的作用 发送网络请求&#xff0c;获取响应数据。 中文文档&#xf…

随机森林(Random Forest)学习

随机森林是一种基于集成学习的机器学习算法&#xff0c;属于Bagging&#xff08;Bootstrap Aggregating&#xff09;方法的一种扩展。它通过组合多个决策树来提升模型的泛化能力和鲁棒性&#xff0c;广泛用于分类、回归和特征选择任务。 1.随机森林核心思想 1.1少数服从多数 在…

从 0 到 1!Java 并发编程基础全解析,零基础入门必看!

写在前面 博主在之前写了很多关于并发编程深入理解的系列文章&#xff0c;有博友反馈说对博主的文章表示非常有收获但是对作者文章的某些基础描述有些模糊&#xff0c;所以博主再根据最能接触到的基础&#xff0c;为这类博友进行扫盲&#xff01;当然&#xff0c;后续仍然会接…

el-input宽度自适应方法总结

使用 style 或 class 直接设置宽度 可以通过内联样式或 CSS 类来直接设置 el-input 的宽度为 100%&#xff0c;使其自适应父容器的宽度 <template><div style"width: 100%;"><el-input style"width: 100%;" v-model"input">…

解决 Supabase “permission denied for table XXX“ 错误

解决 Supabase “permission denied for table” 错误 问题描述 在使用 Supabase 开发应用时&#xff0c;你可能会遇到以下错误&#xff1a; [Nest] ERROR [ExceptionsHandler] Object(4) {code: 42501,details: null,hint: null,message: permission denied for table user…

java每日精进 5.20【MyBatis 联表分页查询】

1. MyBatis XML 实现分页查询 1.1 实现方式 MyBatis XML 是一种传统的 MyBatis 使用方式&#xff0c;通过在 XML 文件中编写 SQL 语句&#xff0c;并结合 Mapper 接口和 Service 层实现分页查询。分页需要手动编写两条 SQL 语句&#xff1a;一条查询分页数据列表&#xff0c;…

linux taskset 查询或设置进程绑定CPU

1、安装 taskset larkubuntu&#xff1a;~$ sudo apt-get install util-linux larkubuntu&#xff1a;~$ taskset --help 用法&#xff1a; taskset [选项] [mask | cpu-list] [pid|cmd [args...]] 显示或更改进程的 CPU 关联性。 选项&#xff1a; -a&#xff0c; --all-tasks…