首页 > 解决方案 > git 有没有办法将提交的更改转储到工作树?

问题描述

我提交了一些更改,其中包含我不想提交的更改,因此我想删除该提交,但保留已提交的分阶段和非分阶段更改,以便我可以在提交之前删除不需要的更改。我使用git reset --hard <hash>但它恢复到 HEAD - 1 处的提交,这不仅删除了提交,还删除了提交之前的所有暂存和未暂存的更改。

有没有办法重置为提交,但将所有提交的更改(返回)转储到工作树,而不是删除该提交中记录的每个更改?换句话说,我怎样才能将所有已提交的更改返回到工作树?

标签: gitgit-commitgit-reset

解决方案


首先,请注意术语索引暂存区域的含义相同。还有第三个术语,缓存,现在主要出现在标志中(git rm --cached例如)。这些都指的是同一个基础实体。

接下来,虽然从更改的角度考虑通常很方便,但这最终会误导您,除非您牢记这一点:Git 不存储更改,而是存储快照。我们只有在比较两个快照时才能看到变化。我们将它们并排放置,就好像我们在玩找不同的游戏一样——或者更准确地说,我们让 Git 将它们并排放置并比较它们并告诉我们有什么不同。所以现在我们看到了这两个快照之间的变化。但是 Git 没有这些变化。它有两个快照,只是比较它们。

现在我们到了真正棘手的部分。我们知道:

  • 每个提交都有一个唯一的哈希 ID,这是 Git 查找特定提交的方式;

  • 每个提交存储两件事:

    • 它包含 Git 在您或任何人制作快照时所知道的每个文件的完整快照;和
    • 它有一些元数据,包括提交者的姓名和电子邮件地址、一些日期和时间戳等等——对于 Git 来说重要的是,它有一些早期提交的原始哈希 ID,所以Git 可以从每个提交到其父提交及时回退;
  • 并且任何提交的所有部分都将永远冻结在时间中。

所以提交存储快照,Git 可以提取这些快照供我们处理。但是 Git 不只是将提交提取到工作区域中。其他版本控制系统有:它们有提交和工作树,这就是全部,以及您需要知道的所有内容。提交的版本一直被冻结,可用的版本是可用的,并且是可变的。这是两个“活动”版本,并为我们提供了一种查看更改内容的方法:只需将活动但冻结的快照与工作快照进行比较。

但无论出于何种原因,Git 都没有这样做。相反,Git 有三个活动版本。一个活动版本一直被冻结,就像往常一样。一个活动版本在您的工作树中,就像往常一样。但是这两个版本之间,还有第三个快照。它是可变的,但它更像是冻结的副本而不是有用的副本。

每个文件的第三个副本,位于冻结提交和可用副本之间,Git 的索引,或者至少是您需要担心的 Git 索引的一部分。1 您需要了解 Git 的索引,因为它充当您提议的下一次提交

也就是说,当您运行时:

git commit

Git 会做的是:

  1. 收集适当的元数据,包括当前提交的哈希 ID;
  2. 制作一个新的(虽然不一定是唯一的2)快照;
  3. 使用快照和元数据进行新的、唯一的提交;3
  4. 将新提交的哈希 ID 写入当前分支名称

此处的最后一步将新提交添加当前分支。上面第 2 步中的快照是此时 Git 索引中的任何内容。所以在你运行之前git commit,你必须更新 Git 的索引。这就是 Git 让你运行的原因git add,即使对于 Git 已经知道的文件:你并没有完全添加文件。相反,您正在覆盖索引副本


1其余部分是 Git 的缓存,通常不会在您面前全部显示出来。您可以在不了解缓存方面的情况下使用 Git。在不了解索引的情况下,很难(甚至不可能)很好地使用 Git。

2例如,如果您进行了提交,然后将其还原,则第二次提交将重新使用您在进行第一次提交之前拥有的快照。重新使用旧快照一点也不异常。

3与源快照不同,每次提交始终是唯一的。了解为什么会出现这种情况的一种方法是每次提交都有一个日期和时间。您必须在一秒钟内进行多次提交,以免它们中的任何一个获得相同的时间戳。即使这样,这些提交也可能具有不同的快照和/或不同的父提交哈希 ID,这会使它们保持不同。获得相同哈希 ID 的唯一方法是在相同的上一次提交之后,由同一个人同时提交相同的源。4

4或者,您可能会遇到哈希 ID 冲突,但这从未真正发生过。另请参阅新发现的 SHA-1 冲突如何影响 Git?


照片

让我们绘制一些提交的图片。让我们使用大写字母代替哈希 ID。我们将沿着主线分支有一个简单的提交链,还没有其他分支:

... <-F <-G <-H

在这里,H代表链中最后一次提交的哈希 ID。CommitH既有快照(无论何时你或任何人提交,都从 Git 的索引中保存H)和元数据(提交人的姓名H等)。在元数据中, commitH存储了较早的 commitG的原始哈希 ID。所以我们说H 指向 G

当然, CommitG也有快照和元数据。该元数据使较早的提交G指向更早的提交FF依次提交更远的点。

这一直重复到第一次提交。作为第一,它不指向回,因为它不能;所以 Git 可以停在这里。Git 只需要能够找到最后一次提交。Git 需要它的哈希 ID。你可以自己输入,但那会很痛苦。您可以将其存储在某个文件中,但这会很烦人。您可以让Git为您存储它,这会很方便 — 这正是分支名称的用途和作用:

...--F--G--H   <-- main

名称仅包含链中 最后一次提交main的一个哈希 ID 。

无论我们有多少名称和提交,这都是正确的:每个名称都包含一些实际有效提交的哈希 ID。让我们创建一个新名称 ,feature它也指向H,如下所示:

...--F--G--H   <-- feature, main

现在我们需要一种方法来知道我们正在使用哪个名称。Git 将特殊名称附加HEAD到分支名称之一,如下所示:

...--F--G--H   <-- feature, main (HEAD)

我们现在 "on" main,并使用commit H。让我们使用git switchorgit checkout切换到name feature

...--F--G--H   <-- feature (HEAD), main

没有其他任何改变:我们仍在使用 commit H。但是我们使用它是因为name feature

如果我们进行新的提交——我们称之为 commit——commitII指向 commit H,Git 会将 commitI的哈希 ID 写入当前name。这将产生:

...--F--G--H   <-- main
            \
             I   <-- feature (HEAD)

现在,如果我们git checkout main,Git 必须交换我们的工作树内容我们提出的下一个提交内容。因此git checkout main将翻转 Git 的索引我们的工作树内容,以便它们匹配 commit H。之后,git checkout feature将它们翻转回来,使它们都匹配 commit I

如果我们在 上进行新的提交Jfeature我们会得到:

...--F--G--H   <-- main
            \
             I--J   <-- feature (HEAD)

reset命令:这很复杂!

git reset命令很复杂。5 我们将在这里只查看命令的“whole commit”reset 变体——带 、 和 options 的命令--hard——--soft--mixed不是那些主要执行我们现在可以git restore在 Git 2.23 及更高版本中执行的操作的命令。

这些“整体提交”重置操作采用一般形式:

git reset [<mode-flag>] [<commit>]

mode-flag--soft--mixed或中的一个--hard6 说明commit符——可以直接是原始哈希 ID,也可以是任何其他可以转换为提交哈希 ID 的东西,通过将其提供给它——git rev-parse告诉我们将移动到哪个提交。

该命令做了三件事,除了你可以让它提前停止:

  1. 首先,它移动HEAD附加的分支名称。7 它只需将新的哈希 ID 写入分支名称即可。

  2. 其次,它将 Git 索引中的内容替换为您选择的提交中的内容。

  3. 第三也是最后一个,它也用它在 Git 索引中替换的内容替换了你的工作树中的内容。

第一部分——移动HEAD——总是会发生,但是如果你选择当前提交作为新的哈希 ID,那么“移动”就是从你所在的位置到你所在的位置:有点毫无意义。仅当您让命令继续执行步骤 2 和 3,或至少执行步骤 2 时,这才有意义。但它总是会发生。

默认值commit当前提交。也就是说,如果您不选择新的提交,git reset则会选择当前提交作为要移动的地方HEAD。因此,如果您不选择新的提交,那么您将在第 1 步中执行“原地不动”的动作。没关系,只要你不让它停在那里:如果你git reset在第 1 步之后停下来,让它留在原地,你做了很多工作却什么也没做。这并没有,但这是浪费时间。

所以,现在让我们看一下标志:

  • --soft告诉git reset做动作,然后停在那里移动之前Git 的索引中的任何内容在之后仍然在 Git 的索引中。工作树中的任何内容都保持不变。

  • --mixed告诉git reset移动,然后覆盖你的索引,但不要管我的工作树

  • --hard告诉git reset移动,然后覆盖你的索引和我的工作树

所以,假设我们从这个开始:

...--F--G--H   <-- main
            \
             I--J   <-- feature (HEAD)

并选择 commitI作为git reset应该移动的地方feature,这样我们最终得到:

...--F--G--H   <-- main
            \
             I   <-- feature (HEAD)
              \
               J

请注意提交J仍然存在,但除非我们将哈希 ID 保存在某处,否则我们无法找到它。我们可以将J' 的哈希 ID 保存在纸上、白板上、文件中、另一个分支名称中、标签名称中或其他任何地方。任何可以让我们输入或剪切和粘贴的东西,或者任何可以做的事情。然后我们可以创建一个新的名字 find J。我们可以在执行之前执行此操作git reset,例如:

git branch save
git reset --mixed <hash-of-I>

会让我们:

...--F--G--H   <-- main
            \
             I   <-- feature (HEAD)
              \
               J   <-- save

其中名称save保留了J的哈希 ID。

--mixed如果我们在这里使用它,它会告诉 Git:根本不要碰我的工作树文件! 这并不意味着您将在工作树中拥有与 commit 完全相同的文件J,因为也许您在执行git reset. 这--mixed意味着 Git 将使用来自I. 但是 Git 不会在这里碰你的文件。只有 with--hard才会git reset触及您的文件。

(当然,如果你运行git checkoutor git switch: 好吧,这些命令应该会触及你的文件,所以这又会变得更加复杂。但现在不要担心,因为我们正在专注于git reset.)


5我个人认为git reset太复杂了,方法git checkout。Git 2.23 将旧的拆分git checkoutgit switchgit restore. 我认为git reset应该同样分开。但现在还没有,所以除了写这个脚注之外,抱怨没有什么意义。

6还有--merge--keep模式,但它们只是我打算忽略的更复杂的情况。

7分离的 HEAD模式下,我在这里忽略它,它只是直接写入一个新的哈希 ID HEAD


概括

默认设置git reset是不理会您的文件 ( --mixed)。您还可以告诉 Git 不理会自己的索引,使用--soft: 当您想要使用 Git 索引中的内容进行新提交时,这有时很有用。假设你有:

...--G--H   <-- main
         \
          I--J--K--L--M--N--O--P--Q--R   <-- feature (HEAD)

I其中的提交Q都只是各种实验,而你的最后一次提交——提交R——所有内容都处于最终状态。

然后,假设您希望使用来自 的快照但在 commit 之后进行提交,并且您希望将其称为(updated) 上的最后一次提交。你可以这样做:RIfeature

git checkout feature      # if necessary - if you're not already there
git status                # make sure commit R is healthy, etc

git reset --soft main     # move the branch name but leave everything else

git commit

在 之后git reset,我们有这张图片:

...--G--H   <-- feature (HEAD), main
         \
          I--J--K--L--M--N--O--P--Q--R   ???

现在很难找到I提交R。但是正确的文件现在在 Git 的索引中,可以提交了,所以git commit我们可以调用一个新的提交S(对于“squash”):

          S   <-- feature (HEAD)
         /
...--G--H   <-- main
         \
          I--J--K--L--M--N--O--P--Q--R   ???

如果您要将快照与 中的快照进行比较RS它们将是相同的。(这是另一种情况,Git 只是重新使用现有的快照。)但是由于我们看不到commits I-J-...-R,现在看起来好像我们已经神奇地将所有提交压缩为一个:

          S   <-- feature (HEAD)
         /
...--G--H   <-- main

S它的 parent相比,我们看到与比较vs时H看到的所有相同的变化。如果我们再也见不到,那可能就好了!HRI-J-...-R

git reset --soft很方便,因为我们可以移动分支名称保留Git 索引和工作树中的所有内容。

在其他一些情况下,我们可能想要从R. 在这里我们可以--mixed重置 Git 的索引:

git reset main
git add <subset-of-files>
git commit
git add <rest-of-files>
git commit

这会给我们:

          S--T   <-- feature (HEAD)
         /
...--G--H   <-- main

中的快照与 中的快照T匹配R,并且中的快照S只有几个更改的文件。在这里,我们使用--mixed重置模式来保持工作树中的所有文件完整,但重置 Git 的索引。然后我们使用git add更新 Git 的索引以匹配我们的工作树的一部分,提交一次到 make S,并使用git add更新我们的工作树的其余部分并再次提交到 make T

所以所有这些模式都有它们的用途,但要理解这些用途,你需要了解 Git 对 Git 的索引和你的工作树做了什么。


推荐阅读