首页 > 解决方案 > git中的reset命令

问题描述

我知道重置命令包含 3 个选项 -

  1. 硬 - 将我们工作目录中的文件更改为特定的提交 ID

  2. 混合(默认) - 取消提交和取消暂存文件

  3. 软 - 仅取消提交文件

我知道 uncommomit - 移动 HEAD 和关联的分支指针,实际上并没有修改提交树,但我不确定这意味着什么?移动我们的头有什么意义?

标签: gitgit-reset

解决方案


从根本上说,该git reset命令做了太多不同的事情甚至不应该存在。(这当然只是我的意见。它确实需要存在,但可能应该由几个不同的管道命令组成,再加上至少三四个瓷命令。实际上有几个瓷命令,例如git merge --abort,那次运行git reset。应该还有更多。)

不幸的是,git reset它确实存在,因为它做了很多不同的事情,它也非常有用。虽然有用,但它——至少是潜在的——具有破坏性。这是一个瑞士军刀命令,但它的刀片不会关闭并且充满破伤风,你需要学习如何小心地握住它,这样你就不会一直刺伤自己,然后死于锁颚

在其更基本的形式中,git reset它的作用是写入 Git 存储库中的一个、两个或三个项目。要理解这一点,您必须首先了解提交和分支是如何工作的,Git 哈希 ID 的功能,以及HEAD索引工作树的角色。让我们从哈希 ID 开始。

哈希 ID

在 Git 中,哈希 ID 如下所示:b5101f929789889c2e536d915698f58d5c5c6b7a. 这是一大串难看的字母和数字。但它实际上是一些数据的校​​验和,特别是加密校验和。这意味着:

  • 它看起来很随机——你无法猜测它会是什么;
  • 它依赖于它的输入数据,因此改变任何东西——数据中的任何一个位,或者数据中位或字节的顺序——都会改变它;但是
  • 宇宙中的每个人都可以对相同的数据进行相同的数学计算并得出相同的哈希 ID。

此散列过程用于获取任何冻结的数据,例如文件内容,并将它们转换为该数据唯一的散列 ID。该 ID 成为冻结数据的简写名称。如果我给你哈希 ID,你可以检查你是否有数据。如果我给你数据,你可以计算哈希 ID。而且,如果我给你哈希 ID数据,你可以自己检查一下我是否在哈希 ID 上撒了谎,或者给了你正确的对。

实际上,这意味着任何两个 Git 都可以聚在一起进行简短的对话:你有 ID X 吗?Y和Z呢? 如果一个 Git 缺少其中一个 ID,另一个 Git 可以给它数据,现在它同时拥有哈希 ID 和数据。如果两个 Git 都拥有所有 ID,则它们拥有所有数据。因此,两个 Git 可以非常快速地相互同步,发送者将发送者拥有而接收者没有的任何东西都提供给接收者。

这本身已经有点用了,但是当我们将它与commits结合使用时,它变得非常有用。

提交

在 Git 中,提交是一个只读实体,它保存文件的快照——所有文件的快照,并且作为快照,而不是对它们的一组更改——加上一些元数据。元数据是关于提交的有用信息:它有提交人的姓名和电子邮件地址,例如,加上时间戳。它还具有一个或多个提交的每个哈希 ID。

因为这是只读数据——因为它不能被改变——我们可以计算这个提交的哈希 ID。这个提交现在永远是这个带有这个哈希 ID 的提交。它的任何数据都不能改变:我们有哈希 ID,它唯一地标识了这个数据,没有其他数据可以使用这个哈希 ID 做任何事情。(对此明显的反对意见,请参阅新发现的 SHA-1 冲突如何影响 Git?

但是,由于提交包含其父提交的哈希 ID 作为其数据的一部分,我们需要做的就是确保我们拥有此链中的每个提交。比如说,我们有一个带有哈希 ID 的提交H

                          <-H

其中之一 H其父提交的哈希 ID。我们称其为 parent G。所以我们确保我们也有那个提交,我们说这H 指向 G

                      <-G <-H

嗯,其中一件事G是其父级的哈希FID ,. 所以我们确保我们也有F,它还有另一个 hash ID E,它有一个 hash ID D,依此类推,一直回到我们在存储库中所做的第一次提交,我们正在调用其哈希 ID A

A <-B <-C <-D <-E <-F <-G <-H

由于A 第一次提交,它没有父级,我们到此为止。

这些箭头被嵌入到提交中:H 总是指向G,因为 的哈希 IDG被嵌入H并且无法更改,当然H它本身也被冻结,其哈希 ID 也永远不会改变。因此,出于绘图目的,我们可以将它们绘制为连接线。请记住,箭头本身来自提交,并返回到提交。当我们制作时,A我们不知道哈希 ID 是什么B,所以A实际上不能指向B; 但是当我们制作时,B我们知道是什么A,所以B指向A.

这意味着给定:

A--B--C--D--E--F--G--H

所有内部箭头都严格向后。我们必须从头开始H并向后工作。如果我们从 开始D,我们可以向后退到C,然后到Band A,但我们实际上不能前进到E

分支、分支名称和HEAD

Git 中的所有分支名称都是一个人类可读的名称,它包含一个单一的哈希 ID。该名称包含的哈希 ID 是该分支中最后一次提交的哈希 ID!

因此,通过上图——提交A通过H——我们可以有一个或多个分支名称指向八个提交中的任何一个。让我们画一些:

A--B--C--D--E--F   <-- master
                \
                 G--H   <-- develop

在这里,名称 通过持有实际的哈希 ID 来master表示提交。我们不需要自己记住那个哈希 ID:我们只需说. 该名称会记住 的哈希 ID 。FFmaster developH

从 开始H,我们可以通过每次提交向后工作。From F,我们可以通过大多数提交向后工作 - 但我们看不到Hand GfromF因为这需要向前推进,这是不可能的。我们迫切需要这个名字develop,这样我们才能找到H,我们从中找到G。之后,只要名称master仍然存在,我们就可以找到F和更早的提交。

在 Git 中,提交AthroughH是 on branch develop,而AthroughF是 on master

我们添加一个名称,也指向F,方法是:

git checkout master
git checkout -b feature

现在我们有了这张图:

A--B--C--D--E--F   <-- master, feature (HEAD)
                \
                 G--H   <-- develop

请注意,没有任何提交发生变化——这很好,因为它们不能。我们刚刚添加了一个新标签,feature也指向F.

我在这幅画中添加了另一件事,即特殊名称HEAD,全部大写字母。 HEAD是 Git 如何记住我们使用的标签的方式。我们让 GitHEAD像这样将我们的分支名称附加到一个分支名称上,这就是我们“打开”的分支。

如果我们现在进行的提交,它会获得一个新的唯一哈希 ID。让我们称之为I. 我们稍后会看看我们做出这个新提交的过程,但现在让我们说“我们做到了”,它现在已经存在了。该图现在看起来像这样:

                 I   <-- feature (HEAD)
                /
A--B--C--D--E--F   <-- master
                \
                 G--H   <-- develop

也就是说,我们的新提交I指向回F名称 feature现在指向新提交I。从I中,我们可以找到F;从F我们可以找到E,等等。我们无法到达GH这条路。提交G并且H仅在. _ master提交I仅在. _ feature提交A-F所有三个分支上。

请注意,特殊名称HEAD仍附加到 name feature。我们没有改变我们所在的分支。我们根本没有更改任何现有的提交。我们添加了一个提交,并且我们更改存储在 name 中的哈希 IDfeature

恭喜!您现在了解了 Git 分支。一个分支一系列提交,通常从末尾开始并向后工作。你可以选择你想往后走多远——你可以一直走到一个根提交,这是一个没有父母的提交。

分支由分支名称标识,该名称包含作为分支一部分的最后一次提交的哈希 ID。请注意,当人们说分支这个词时,他们通常指的是分支名称而不是提交字符串。所以分支这个词在现实生活中是模棱两可的,你需要一些上下文来确定某人是指commitsname还是两者兼而有之。另请参阅“分支”到底是什么意思?

HEAD同时,像这样全部大写的特殊名称通常附在一个分支名称上。您的选择是将其附加到分支名称,git checkout用于执行此操作,或者将其与分支名称分离,也使用git checkout. 我们不会在这里进入这种 detached-HEAD 模式,只是说在这种模式下,名称HEAD只是包含某个提交的实际哈希 ID。但是,在正常使用中,的作用HEAD是记住我们使用的是哪个分支名称

请注意,当您使用git checkout分支名称时,Git 将附加HEAD到分支名称。该名称找到的提交(例如,commit Ffor master)现在是您当前的提交。因此,要查找当前分支名称,我们询问 Git附加了什么名称HEAD,而要查找当前提交,我们通过某个分支名称询问 Git指向哪个提交?HEAD 该名称HEAD处理这两种操作,具体取决于我们提出的问题。我们问“分支名称是什么”或“提交哈希是什么”,我们会得到适当的答案。

索引和工作树,或者我们如何构建和使用提交

现在是时候看看索引和工作树在使用 Git 时的作用了。

在 Git 中,每个存储库(除了--bare一个)都有一个索引和一个工作树,用于处理所有冻结的提交和分支名称。如您所知,提交保存了所有文件的快照。但是提交的东西——关于提交本身的元数据,加上所有文件的名称和内容的副本——<em>所有这些东西都被永远冻结了。没有一个可以改变,一点也不能改变。这对于存档和探索过去非常有用,但它不允许我们完成任何工作。

冻结的内容和名称也被压缩,有时是高度压缩的。(在制作快照后,Git 稍后会使用一种称为delta 压缩delta 编码的技巧。哈希 ID 不考虑 delta 编码,因此它会根据需要在不可见的情况下完成和撤消,您可以假装 Git 没有拥有它。)因为它们被冻结了,所以它们也可以共享:你可以有两个提交,或者十个,或者一百万个,它们都具有某个非常大的文件的相同副本,在这种情况下,它们都共享一份压缩副本。这意味着由于您在大多数提交中大多不会更改大多数文件,因此大多数提交大多只是共享他们冻结的副本,而不占用额外的空间。

同样,这非常适合归档——但要完成工作,我们需要一种方法来解冻冻结的提交。我们需要提取冻结文件名的冻结内容。我们需要将冻结的 Git 化文件解冻并解压缩到一个可以处理和使用它们的地方。该位置是工作树(或工作树工作目录或此类名称的某种组合)。

工作树是您可以查看文件并对其进行处理的地方。它包含这些文件的副本,这些文件是 Git 从某个冻结的提交中提取的——从您使用git checkout. 选定的提交是您当前的提交。当然,这些文件适用于所有普通的非 Git 计算机程序,因此您可以在此处更改它们。您可以创建新文件或删除文件。简而言之,您可以这个目录树(这组文件夹和子文件夹)中工作,这就是为什么它是您的工作树。

Git可以在这里停止,在当前提交中保存一份文件的冻结副本,并在工作树中保存第二份可用且可更改的文件副本。其他版本控制系统确实到此为止,但 Git 不同。Git 会生成文件的第三份副本。这第三个副本就是 Git 所说的index

索引是 Git 跟踪您的工作树的方式。实际上,索引中文件的存在是文件被跟踪的原因。如果README.md提交中有类似的文件,Git 会将其复制到索引中,然后将其从索引中复制到工作树中。现在README.md所有三个地方都是如此,因为它在索引中,所以它被跟踪了。

索引中的文件采用特殊的、压缩的、仅限 Git 的形式。他们已准备好冻结的提交。Git 要求这始终是正确。因此,如果您更改工作树中的文件,Git 会强制您git add将文件复制回索引中,以更新索引副本。这会重新压缩和 Git 化文件的内容,以便它可以冻结。

这意味着该索引实际上是建议的下一次提交。将文件复制索引中意味着将工作树中的内容作为建议的内容。

git checkout第一次签出某个提交时,为了使其成为当前提交,Git 将文件复制到索引中,以便提议的提交与当前提交具有​​相同的文件和相同的内容。然后,您可以在工作树中尽可能多地调整文件,然后使用git add将其复制回来,以更新建议的提交。

该复制回步骤正在暂存文件。如果提议的提交还没有更新的文件,则该文件不是暂存的,因为如果您git commit现在运行,那将使用该文件的副本,即从提交中出来的那个。将文件从工作树复制到索引/暂存区域后,建议的提交现在具有副本。

因此,每个文件都有三个活动副本!当前提交中有冻结的;索引中有第二个副本;工作树中有第三个副本。您必须使用类似git show或其他一些 Git 命令来查看仅 Git 的副本:

git show HEAD:README.md     # view the frozen current-commit copy
git show :README.md         # view the index copy

当然,您可以使用普通的计算机命令来查看普通的工作树副本,README.md. 但是由于存在三个副本,因此所有三个副本都可能不同。通常,它们中至少有两个是相同的,因为索引开始时与冻结的提交相同,然后git add使其与工作树相同。但它们都可以不同。

无论如何,当我们对 进行新的提交Ifeature,我们这样做的方式是:

git checkout master
git checkout -b feature
... do some work ...
... run `git add` on our changed files ...
git commit

git checkout master步骤将我们附加HEAD到名称master并将提交提取F到我们的索引和工作树中。该git checkout -b feature步骤创建了新名称feature,指向提交F,并附HEAD加到这个新名称。我们的索引和工作树仍然匹配当前的提交F

然后,我们做了一些工作。这改变了我们工作树中的文件。然后我们运行git add它们,将它们复制回索引中。最后,我们跑了git commit。提交命令接受了我们的详细信息——我们的姓名和电子邮件地址、当前时间、解释我们为什么进行此提交的日志消息等等。它将当前提交的哈希 ID 添加为新提交的父级。它冻结了索引以保存所有文件。然后它将所有这些数据——关于提交的所有元数据,以及通过具有另一个哈希 ID的树对象的冻结文件——作为新提交存储到 Git 存储库中。

F新提交及其冻结的文件树和冻结的元数据,像往常一样指向现有的提交。它有一个新的哈希 ID——它现在是我们的提交——并且 Git使用“HEAD 有哪个分支名称”问题I将这个新的哈希 ID 写入当前名称。所以现在这个名字feature指向了新的提交I

现在我们了解了以上所有内容,现在我们可以解释git reset

正如我上面提到的,该git reset命令以三种主要形式——<code>git reset --soft,git reset --mixedgit reset --hard——写入一个、两个或三个东西。

git reset写入或可以写入的三件事是:

  1. 当前分支名称中存储的哈希 ID;
  2. 索引;和
  3. 工作树。

最软的复位类型git reset --soft,写入第一部分,然后停止。也就是说,它将哈希 ID 写入分支名称。索引和工作树不受干扰。

混合类型的复位写入#1#2,然后停止。也就是说,它将哈希 ID 写入分支名称,然后将一些内容写入索引。但是,工作树没有改变。

硬复位写入#1、#2 和#3。也就是说,它将哈希 ID 写入分支名称,然后将一些内容写入索引,然后也写入整个工作树。

所以,我们对硬、混合或软标志所做的就是告诉git reset 何时停止。它将始终写入当前分支,使用名称HEAD来确定是哪个分支名称。它可能会写入索引,如果这样做,它可能会在整个工作树上写入内容。

但是我们还需要查看 git reset这些中的每一个的写入内容,这就是事情变得复杂和有用的地方。

您告诉git reset在分支名称中存储哪个哈希 ID

文档SYNOPSIS部分部分git reset

git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]

我们将忽略这里的和--merge标志。or " quiet" 标志只是关闭了。这里的关键是最后一部分,可选的commit。它是optional,但如果我们忽略它,Git 会假设我们的意思是HEAD--keep-N-qgit reset

Git 对最后一个参数所做的是找到一个提交哈希 ID。因此,如果我们说,例如:

git reset --hard b5101f929789889c2e536d915698f58d5c5c6b7a

我们给了 Git 一个哈希 ID,Git 只是检查以确保它是有效的并命名一个提交。或者我们可以说:

git reset --hard master

在这里,Git 获取名称 master并将其转换为哈希 ID。在我们上面的示例存储库中,这将是任何哈希 ID 提交F。我们也可以说:

git reset --hard HEAD~1

HEAD;倒数一次提交 在我们的示例存储库中,它可能从 开始I并后退到F,因此与此处具有相同的含义master

事实上,您可以使用 Git 可以转换为哈希 ID 的任何内容。翻译过程在gitrevisions 文档中有所描述。无论您使用什么,Git都会将其转换为哈希 ID,并确保哈希 ID 是现有提交的 ID。

如果你忽略它,Git 会使用HEAD. 当然,这意味着我们现在签出的任何提交。要转换HEAD为哈希 ID,Git 首先找出附加到哪个分支名称 HEAD,然后找出该分支名称包含的哈希 ID。

现在我们有了哈希 ID,Git 将其写入当前分支

一旦git reset有了哈希 ID,它就会更新当前分支(即HEAD附加的分支),方法是将您提供的哈希 ID 写入当前名称。因此,如果我们在feature此处的示例中位于分支上,并且您运行:

git reset --soft master

Git 会将提交的哈希 ID 写入F名称中feature

                 I   ???
                /
A--B--C--D--E--F   <-- master,  feature (HEAD)
                \
                 G--H   <-- develop

请注意,提交I 仍然存在。只是我们再也找不到了。 没有名字,我们必须IF. 我们不能前进;Git 不会那样做。(如果我们继续前进,我们不会到达G吗?好吧,也许。这是另一个练习的问题,也许改天。)如果你*保存了I某个地方的哈希 ID,你现在可以运行:

git reset --soft <hash-ID-of-I>

把东西放回去。如果没有,那么一些技巧可以找到I的哈希 ID。其中一些甚至非常容易。一个是这样的:

git reset --soft feature@{1}

因为 Git 会自动将分支的旧 ID 保存在分支的reflog中。(其余的查找方法I,我们留待其他问题。)

更强大的重置

现在,假设git reset --soft master您只是运行而不是 :

git reset --soft

withfeature仍然指向I? 然后 Git 将通读HEAD以找到 的哈希 ID I,并作为其重置操作,将此提交哈希 ID 写回feature. 我们将在我们开始的地方结束:

                 I   <-- feature (HEAD)
                /
A--B--C--D--E--F   <-- master
                \
                 G--H   <-- develop

Git 从 中读取I的哈希 ID feature,然后将其写回feature,保持feature不变。因此,如果没有哈希 ID 说明符git reset --soft则无效。

怎么样--mixed?在这里,Git 将继续写入index。请记住,索引是我们提议的下一次提交:它包含我们可能更新的每个文件的副本。它的作用是git reset --mixed它使用哈希 ID重新设置当前分支后,它会将我们选择的提交中的文件复制到 index中。

因此,如果我们I像这样提交,我们使用:

git reset --mixed HEAD

我们告诉 Git:将哈希 ID 复制feature到中feature,保持feature不变;然后,在你这样做之后,将提交的内容复制I到索引中。 第一步实际上并没有改变任何东西,但第二步具有“撤消”git add我们运行的任何内容的效果。

换句话说,通过保持feature不变,但重新设置索引,我们取消了我们git add的 s。

请注意,如果我们使用了git reset --mixed master,我们将feature指向F(not I)更新索引以匹配 commit F(not I)!所以git reset很强大。我们已经上演的一切都消失了,取而代之的是提交中的内容。当然,如果我们通过从 work-tree 复制来上演,我们仍然在 work-tree 中拥有它。

最后,我们可以使用git reset --hard. 这与之前的前两个步骤相同。也就是说,它首先将一个新的哈希 ID 写入当前的分支名称,如果我们不说要使用哪个,它就是的哈希 ID,并且保持分支名称不变。然后它使用分支名称现在标识的任何提交来重新设置索引。我们上演的一切都消失了。最后,它写入工作树1

当然,这是最强大的重置,因为它破坏了我们在工作树中所做的任何更改……而且这并没有保存在 Git 的任何地方。工作树中有我们的工作。如果我们失去了它,它就消失了。所以从某种意义上说,git reset --hard是最危险的情况。

但是还记得I当我们不小心重置时我们是如何丢失提交的,即使使用--soft,并且没有先将哈希 ID保存在某个地方?所以即使是软复位也是危险的。幸运的是,Git 倾向于将哈希 ID 保存在很多地方——但事实证明,这与根本不保存它们几乎是一样大的问题。如果您需要查找丢失的哈希 ID,您可以查看 reflogs。但是那里通常有成百上千的哈希 ID,而且它们看起来都是完全随机的。找到合适的,大海捞针,可能会非常乏味。

最后,git reset它是强大而有用的,但也非常危险。永远小心它。要特别小心--hard,或任何更改哪个提交哈希 ID 存储在分支名称中。默认值是将取自 (the name given by) 的哈希 ID 写入( the HEADname given by) HEAD,它什么也不做,这使它安全。


1我在这里省略 git reset --hard写入工作树的内容。那是因为这部分有点复杂。对于技术上的好奇,请参阅文档git read-tree但总结一下:Git 更新索引时,索引告诉 Git 它应该假设哪些文件在工作树中,因为它们在索引中。这些是 Git 可以而且应该破坏的文件。换句话说,未跟踪的文件大多没有被触及。从您要重置的提交中,跟踪的文件将被替换为新的跟踪文件。但是这里有一些极端情况:假设文件X在旧提交中被跟踪,而根本不在新提交中。然后 Git 应该删除 X从工作树。同样,假设文件Y未在旧提交中跟踪,但新提交中。然后 Git 应该在工作树中创建 Y——但可能已经有一个未跟踪的文件Y;Git应该破坏它吗?(Git 通常不会,但有时一个.gitignore条目会授予 Git 访问 clobber 文件的权限Y。)


推荐阅读