Github入门4 - 撤销与回滚

[原文链接:Link] 转载请注明原作者


🍻Git 后悔药

🍬基础药方:分区看待,对症下药

♻️ 在工作区进行 Ctrl + Z
  • ⚓情况:想把一个 modified 文件变成 unmodified 文件 (就是上次 commit 后的干净的文件)

  • 方式:

    • (旧)git checkout -- <file> ... (Git 2.23.0 版本前 - 2019-8月前)

    • (新)git restore <file> ... (Git 2.23.0 版本后 - 2019-8月后)

      (以上基础信息基本只在 $ git commit 前决定是否需要使用)

♻️ 在暂存区进行 Ctrl + Z
  • ⚓情况:想把一个 staged (已暂存) 文件变回 unstaged 文件,也就是变回仅 modified 但未暂存的文件

    • (旧)git reset HEAD <file> ... (Git 2.23.0 版本前 - 2019-8月前)

    • (新)git restore --staged <file> ... (Git 2.23.0 版本后 - 2019-8月后)

      (以上基础信息基本只在 $ git commit 前决定是否需要使用)

♻️ 在版本库进行 Ctrl + Z
  • 目的:回滚某个 commit

    • 首先,并不是所有情况都需要回滚commit

      因为正常代码改错了的话,重改完 add 再重新 commit 就好了

    • 所以,如下情况才需要回滚commit

      • (⚓情况1) commit message 写错了,日志被污染

        • 解决方案:
          • git commit --amend (重新修改上一次的 commit message) (amend v. 修正)
      • (⚓情况2) commit 前忘记检查 status, 落下了几个 modified but unstaged 文件忘了 commit

        • 解决方案:

          • git add <当初落下的未暂存的那些modified文件>

          • git commit --amend (重新修改上一次的 commit message,并补上落下的文件)

        关于 git commit --amend:

        其中 –amend 本质是另开一个分叉,把上一个 commit 全部内容进行修正(在新分叉上),

        旧的那个分叉就舍弃掉了,只是会留在 git reflog 的记录里,但 git log 再也看不到它了。

        此外,$ git commit --amend 等价于 $ git reset --soft HEAD~ (见下节内容)

        此外!!

        如果你的 commit 已经 push 到了远程仓库,那么使用 --amend 修改 commit 后,

        git push 时一定要使用 --force-with-lease 或 -f 参数。否则就会报错。

        下节内容同理,一旦回溯,push 则需加 -f 参数。

      • (☠️情况3) commit 了严重错误的内容,或不小心 commit 进去了一堆垃圾文件

        • 解决方案:
          • ☠️ 超出 $ git commit --amend 能力范围,需要其他指令来完成 (引出下一节内容)

🍬高级药方:$ git reset 的三组药方

  • 学完对于三个分区的操作回滚(撤销),你会发现自己对 commit 的回滚管理好像还不能做到尽善尽美

  • 这时就需要另外一套解决方案来发挥发挥了,那就是 reset 。

  • 其本质是: 对 HEAD 指针及其 branch 位置的更改(主要目的),及其他区域信息的更改(次要,由参数决定)。

    git-reset - Reset current HEAD to the specified state

    故 reset 并不是撤销, 而是让 HEAD, branch 位置重设

  • 另一解读: 由上部分中 $ git commit --amend 引出,reset 通过开分叉,扔掉另一叉不要的内容。

    注意:reset 是针对 commit 层面,进行三种程度的撤销。

    这点与上一节不同,上一节是针对三个区域进行撤销

  • reset 操作分三种模式,但每种都需要小心使用,或仔细阅读各个细节后再决定是否使用,具体图示如下:

    git后悔药

🍭 git reset --soft (常用:回滚 commit )
  • Soft Reset - 软重置:

    • 重设 HEAD 指针位置,重设 branch 位置,到一个 未commit 的分叉节点上。
    • 不回滚本地工作目录内容
    • 不回滚暂存区内容

    🍕一句话概括:啊刚不小心误提交了,撤回撤回,就当没发生过 git commit 。

    ※ UNDO COMMIT ONLY

  • 应用场景:

    • 只是为了撤销一步 $ git commit ,继续改改时
    • 写错了 commit message 时
    • 要整合多个 commits 为 1 个 commit 时
  • 案例:

    • SoftReset 一般只用于前移 1 次,多于 1 次的话,建议使用 HardReset(避免文件混乱)
  • git reset --soft HEAD^ (让 HEAD 指针带领当前 branch 前移 1commit 对象,再开个分叉出去)

    • git reset --soft HEAD~ (让 HEAD 指针带领当前 branch 前移 1commit 对象,再开个分叉出去)
🍭 git reset --mixed(默认:回滚 commit 、回滚 add )
  • Mixed Reset - 默认(混合)重置:

    • 重设 HEAD 指针位置,重设 branch 位置,到一个 未commit 的分叉节点上。
    • 不回滚本地工作目录
    • 回滚暂存区(其实你再 add 回来就行,因为工作目录没动过

    🍕一句话概括:撤回刚才的 commit, 撤回刚才的所有 add操作,但不要碰我写过的代码!

    ※ UNDO COMMIT, UNDO ADD.

  • 应用场景:

    • 撤销本次 commit 中的全部 add 操作,

      如上一个 commit 中不小心扔进去了一些无关的垃圾,用 mixed reset 拿回来,重 commit。

    • 要整合多个 commits 为 1 个 commit 时(但需要额外 add 回滚前的内容)

  • 常有疑问:

    • 如果我回滚暂存区之前,暂存区还有 未commit 的新改动,比如改了 a.java 张三版, 要回滚 a.java 李四版的话,回滚结果会怎样?
      • a.java 李四版 会 彻底覆盖 a.java 张三版 (于暂存区)
      • 但若你此时再输入一次 $ git add a.java(张三版) , 即可再把暂存区更新为张三版。
      • 当然,你也可以通过 $ git cat-file -p <暂存区李四版hash> 查看李四版的内容,
      • $ git diff --staged 来查看。
  • 案例:

    • git reset (让 HEAD 指针带领当前 branch 前移 1commit 对象,再开个分叉出去)
    • git reset HEAD (同上)
    • git reset HEAD^ (同上)
    • git reset HEAD~ (同上)
    • git reset --mixed (同上)
    • git reset --mixed HEAD (同上)
🍭 git reset --hard (危险:回滚 commit 、回滚 add 、回滚全部代码)
  • Hard Reset - 硬重置:

    • 重设 HEAD 指针位置,重设 branch 位置, 到一个 未commit 的分叉节点上。
    • 回滚工作目录,删除回滚前工作目录的所有内容
    • 回滚暂存区,删除回滚前暂存区的全部内容

    🍕一句话概括:这个 commit 里的全部内容都是垃圾,三区全扔,可别让我再看它了。

    ※ UNDO COMMIT, UNDO ADD, UNDO EDIT!

    ※ UNDO ALL!!!

  • 应用场景:

    • 要放弃目前本地的所有改变時
    • 真的想抛弃目标节点后的所有 commit 时
    • 认为该内容过早地进入主分支时
    • 想要撤回 merge 操作时
    • 想要撤回回滚时
  • 常有疑问:

    • 切换分支的时候也会更改工作目录,暂存区,但 git checkout <branchName>git reset --hard <branchName> 底层功能一样吗?
      • 不太一样, reset 会连带 HEAD 和 branch 一起挪,而 checkout 只挪 HEAD。
      • 并且 checkout 前 git 会监测工作目录是否 clear (就是是否全文件已 commit)从而确保了一定安全,不会丢失文件。
  • 案例:

    • git reset --hard HEAD^ (让 HEAD 指针带领当前 branch 前移 1commit 对象,再开个分叉出去)

    • git reset --hard HEAD^^ (让 HEAD 指针带领当前 branch 前移 2commit 对象,再开个分叉出去)

    • git reset --hard HEAD~ (让 HEAD 指针带领当前 branch 前移 1commit 对象,再开个分叉出去)

    • git reset --hard HEAD~ 2 (让 HEAD 指针带领当前 branch 前移 2commit 对象,再开个分叉出去)

  • 杀手级应用

    • git reset --hard <commitHash> (让 HEAD 带领当前 branch 移动到指定 commit 对象上,再开个分叉出去)

    • git reset --hard <branchName> (将 HEADbranch 移动到其他的任何分支所在的 commit 上,再开个分叉出去)

      🧛 比较离谱的操作,有时用在 撤回回滚

      想获取要回滚的 commit hash, 输入 $ git reflog 即可


🎐 再回顾! 基础药方
♻️ 暂存区 Ctrl+Z 细化: git reset HEAD <file> ...

了解了三个 reset 功能,和场景案例。现在回顾一下 “(旧)暂存区回滚” 的那条指令。

  • git reset HEAD <file> ...

你会发现其实也是 reset,全称是:

  • git reset --mixed HEAD <file> ...

※ 它是 git reset HEAD~ 的 [不完全体] ,只用于撤回部分文件。

但为了逻辑清晰,以后还是整体用 git restore(引出下一节内容)

♻️ 工作区 Ctrl+Z 细化: git checkout HEAD -- <file> ...

回顾一下 “(旧)工作区回滚” 的那条指令。它其实有点问题。

测试一下就能发现,它不仅仅回滚工作区,还会回滚暂存区!☠️

也就是说,你在 add a.java 后,又改变了 a.java 的内容,还没来得及重新 add。

这时候你使用 git checkout HEAD -- a.java同时复原工作区及暂存区内容。

比较奇怪,也比较危险。

但新版 git 推出的 git restore <file> ... 指令可以单独复原工作区 ✔️

在上述情况中不会出现那种令人匪夷所思的结果。所以还是用新的指令好。


好了,现在 reset 全部内容了解完毕,现在再回头看看示意图,是不是大体能理解多了?😆

(其余更多骚操作可以从顶部一手教材中继续了解)


🍬 最新药方:$ git restore 与 $ git switch

Git 2.23.0 版本后 - 也就是2019-8月后,Git 发布了两条新指令 restoreswitch

restore 和 switch 的诞生背景:

  • checkout 命令承担了太多功能,对新老手造成了很多认知负担
  • 故从其功能中抽出 restore 和 switch, 以捋清逻辑。

但官方 doc 截至现在 (2020年底) 还写着如下内容:

THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.

总之打好预防针,未来这两条指令的行为可能会继续改变。

🕹️ git restore :逻辑更清晰的单文件回滚工具
  • 案例:

    从上节中我们已经了解了 git restore <file> ... 可以回滚工作区 (working-tree) 的内容

    如回滚 a.java 到上次刚 commit 完的干净状态:

    • git restore ./a.java

    但其实它的全称应改写为 (两种写法均可):

    • git restore --source=HEAD~1 --worktree ./a.java (加等号更清晰)
    • git restore --source HEAD~1 --worktree ./a.java

    虽然,git 的工作人员爱这么写:(但是不推荐简写,好背但难读,谁读谁知道)

    • git restore --s@ --W ./a.java
  • 研究:

    不过现在,通过全称,我们可以进一步的了解 git restore 的工作方式了:

    • 选择回滚文件源

      • --source
        • --source 是把某个 commit 的该文件内容拿回来,让它覆盖你想要覆盖的地方
        • 默认为--source=HEAD~1,意思是不写本参数,默认拿上个 commit 内容当文件源
    • 选择回滚目标区域

      • --worktree
        • 把 source 覆盖到工作区 (默认参数!)
      • --staged
        • 把 source 覆盖到暂存区
      • --worktree --staged
        • 两个都覆盖

      说是覆盖,可能和你想的回滚有点区别,但他只是用覆盖来实现了回滚。

      换句话说,只要你指定了 source 是从哪个地方取文件源,那么你 前滚后滚 都可以。

      且不要忘了 git 全部指令都支持正则表达式,可以批量滚。(如 *.java 滚所有 java 后缀文件)

🕹️ git switch :逻辑更清晰的分支切换工具

这段内容其实更应该写到 “Github入门3 - 分支基础” 中,但介于它是和 restore 一起从 checkout 里拆分出来的功能,就在这里一起解释一下。

  • $ git switch <branchName> 同理于 $ git checkout <branchName>

  • $ git switch -c <newBranchName> 同理于 $ git checkout -b <newBranchName>

  • $ git switch --create <newBranchName> 同上

  • 仅此而已。

  • 不过除此之外, switch 还拆分出了几个逻辑清晰的命令,来帮助你进行分支管理:

    • $ git switch - 切回之前的分支 (常用)

    • $ git switch -c <newBranchName> HEAD~3 从 HEAD 的前三个 commit 上开分叉,并创建分支!

      (是个 功能三合一 的高级指令,与如下方式相同:)

      1. git status 先确认 working tree clear (确保文件不丢失)
      2. git reset --hard HEAD~3
      3. git branch <newBranchName>
      4. git switch <newBranchName>

      (但建议还是一步步来,底层才稳。。。毕竟,高处不胜寒)

    • $ git switch -c <newBranchName> <commitHash> 同上,且通过精准定位 commit 来作为 start-point

    • $ git switch --orphan <new-branch> 创建 **孤儿分支 **(应用不多,仅供了解 Link


🎐 再回顾! 回滚中常用的 HEAD~3 与 HEAD^^ 的用法总结

在你读 git-scm 提供的官方 doc 文件中,你会看到指令如下:

git switch [<options>] (-c|-C) <new-branch> [<start-point>]
git branch [--track | --no-track] [-f] <branchname> [<start-point>]
git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]

其中这个参数:

  • [<start-point>] 中的 <start-point>

那么,在这部分,咱们总结一下这个区域的可用语法:

  • HEAD~2 : 回滚到 HEAD 的 2 commits ago
  • HEAD^^ : 回滚到 HEAD 的 2 commits ago
  • HEAD@{2}: 回滚到 HEAD 的 2 commits ago

☠️ 吃错药的结果:游离态 HEAD

不过,学了两天发现无论是 checkout, switch, restore,还是reset,个个都离不开对 HEAD 的操控。

相比这时候你也应该发现 HEAD 头指针在 git 中的重要程度了。

但玩 HEAD 归玩 HEAD, 要记住 HEAD 有一个特殊状态: 游离态 HEAD (DETACHED HEAD)

你有没有想过, HEAD 每次的移动,不是它带着 branch 跑,就是它自己跑到别的 branch 上。

但还有一种可能, HEAD 错误地跑到了一个没有 branch 的赤裸裸的 commit 上?

那么明确的告诉你,能存在。 且仅在 checkout 中存在。

  • 因为其它三个:
    • switch 避免了直接将 HEAD 移动到 commit,
    • restore 不会移动 HEAD 位置,
    • reset 会带着 HEAD 和 branch 一起跑。
  • 而 checkout, 比较傻

看一看 checkout 的 doc,你会发现:

git checkout [-q] [-f] [-m] [--detach] <commit>
git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]

这两条指令 简化后可理解为:

git checkout <commit>
git checkout [<start_point>]

这是 checkout 功能 过度复杂化 的后果。也是为什么 git 从 checkout 里抽出了 switchrestore 的另一个可能性。

当你输错了 checkout 指令,后面跟的是 commitHash 的时候,令人匪夷所思的事情就会发生了:

游离态 HEAD 会带领一个没有分支的 commit 一直往前走。等你回过神来,你发现你已经脱离工作分支写了老远代码了。

但此时你发现你仿佛处在一座孤岛, $ git status 顶端提示着红字 HEAD detached from <hash>

再切回工作分支的话,那段代码是不是就相当于作废了?

这时候该怎么办?

很简单,在游离的 commit 处新建个临时 branch,然后 merge 回去就好了 😏

不过不要忘了 git 开发 checkout 时,对其本质的描述:

Updates files in the working tree to match the version in the index or the specified tree. If no pathspec was given, git checkout will also update HEAD to set the specified branch as the current branch.

更新本工作区中的文件,来匹配暂存区,或指定树对象中的版本。

但如果没有给出具体路径,$ git checkout 会更新 HEAD,并将其指定的分支设置为当前分支。


🍬 其他偏方:$ git revert

官方定义:

  • revert功能1:回滚暂存区、工作目录的更改

  • revert功能2:提交一个用于记录此次更改的新 commit。

  • 执行需求:需要工作目录是干净的(HEAD 后 再无未 commit 的新变动)

案例1:

$ git revert HEAD~3
在 HEAD 中回滚到 它倒数第四次 commit 中的内容(包括工作区与暂存区),
并创建一个新的 commit 来记录此次更改。

案例2:

$ git revert master~5..master~2 -n
从 master 中 (包含) 第五次 commit ,到 master 中 (不包含) 第二次 commit 所做的所有更改全都恢复原状,
不创建 任何新的 commit 来记录此次更改(-n参数的作用)(类似于 $ git reset --hard)

目的:

  • 真正意义上的前进式回滚,在明确某 commit 真的要舍弃,但又要保留回滚记录时(让回滚记录能用 $ git log 直接看到,而不单单再是从 $ git reflog 才能看到),可以使用 revert 功能。

🌌 在线给药:远程回滚

众所周知, git 分为 localremote 两大部分,也就是 Git的本地3个区域 (Local), 与 Github (remote)

由于本章节前未讲到 remote 部分,故远程回滚内容将在 本人笔记的 remote 一章内进行进一步补充。(于 push 冲突的解决方案部分)


😄 望这篇笔记能对你也有所帮助,若是喜欢,也不妨点一下收藏,万分感谢。

如有错误,也尽请指出。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值