【问题标题】:How could git reset --soft reset last commit without touching index file?git reset --soft reset last commit 怎么能不接触索引文件?
【发布时间】:2014-05-23 13:30:46
【问题描述】:

这可能是一个菜鸟问题。

假设我有一个 Git 存储库,它已经使用 git add 在暂存区域中有一些文件。然后我做一个git reset --soft @~

我很高兴看到我上次提交的一些文件现在被放入暂存区。

但是怎么做呢?我检查.git 文件夹。唯一改变的是当前分支的参考。还有一个我认为不相关的“ORIG_HEAD”。最可疑的索引文件根本没有被触及。还有谁能告诉我如何查看它的内容?

那么 git 怎么能做到这一点呢?谢谢。

【问题讨论】:

    标签: git git-reset git-add


    【解决方案1】:

    在最简单的形式中,1git reset 做了两件事:

    • 移动当前分支,和/或
    • 撤消索引中的内容

    要了解它的工作原理和原因以及它的作用,您需要至少在相对较高的层次上了解提交的工作原理以及索引的工作原理。无论如何,这些都是紧密联系在一起的。

    提交、树和 blob

    首先,commit 只是一个“commit”类型的存储库对象,它的数据、提交消息和一些其他信息(树、父母、作者和提交者)作为其数据:

    $ git cat-file -p 5f95c9f850b19b368c43ae399cc831b17a26a5ac
    tree 972825cf23ba10bc49e81289f628e06ad44044ff
    parent 9c8ce7397bac108f83d77dfd96786edb28937511
    author Junio C Hamano <gitster@pobox.com> 1392406504 -0800
    committer Junio C Hamano <gitster@pobox.com> 1392406504 -0800
    
    Git 1.9.0
    
    Signed-off-by: Junio C Hamano <gitster@pobox.com>
    

    此提交是 git 源代码的一部分(它是 git 版本 1.9.0 的提交)。与所有存储库对象一样,它的名称是 40 个十六进制字符的 SHA-1 值。

    提交的工作目录tree 确定,这是另一个 git 对象,因此它具有另一个 SHA-1 名称。 git cat-file -p 972825cf23ba10bc49e81289f628e06ad44044ff 的输出太长,无法完整包含,但开头为:

    100644 blob 5e98806c6cc246acef5f539ae191710a0c06ad3f    .gitattributes
    100644 blob b5f9defed37c43b2c6075d7065c8cbae2b1797e1    .gitignore
    100644 blob 11057cbcdf4c9f814189bdbf0a17980825da194c    .mailmap
    100644 blob 536e55524db72bd2acf175208aef4f3dfc148d42    COPYING
    040000 tree 47fca99809b19aeac94aed024d64e6e6d759207d    Documentation
    100755 blob 2b97352dd3b113b46bbd53248315ab91f0a9356b    GIT-VERSION-GEN
    

    这些 blob 条目是构成 git 源的所有文件(和子目录,每个 tree;那些有更多 blobs)。每个blob 都有一个唯一的SHA-1 ID,基于文件的内容。 tree 保留了文件的“模式”列表(实际上只是它的x 位——这些模式都是100644100755)和文件名以及 blob 对象的 SHA-1 名称在存储库中。 (其他模式,如上面看到的040000,跟踪子树、符号链接和子模块。只有blob 被限制为100644100755。)

    每个 git 存储库对象都是只读的。 ID 为5f95c9f... 的提交永远不会改变。它的(单一)tree 将始终具有 ID 972825c...。 ID 为536e555... 的文件始终是文件COPYING 的特定版本。如果文件被更新,一个新的、不同的带有新的、不同的 SHA-1 的 blob 会进入。

    索引

    Git 的“索引”(也称为“暂存区”,有时也称为“缓存”)是一个文档记录不充分的文件,其本质上代表了“下一次提交的内容”。

    与存储库对象不同,索引是可写的。为了使“下一次提交”有一些不同,git 从索引中添加或删除条目。例如,要更新名为COPYING 的文件,您将在编辑它之后运行git add COPYING。这将获取文件 COPYING 的新内容并将它们复制到存储库中(它们最终将永远存在),2 为结果计算 SHA-1“真实名称”。然后,这个新的 SHA-1 进入索引(​​连同模式和名称 COPYING — 基本上是提交所需的所有内容)。

    提交

    因为索引已经像这样准备了所有内容,所以很容易进行新的提交。所有正确的blobs 已经在存储库中。 Git 只需要将索引转换为一些tree 对象,将它们写入存储库,获取最新顶级tree 的最终SHA-1,并写入一个新的commit 对象。新提交将具有以下属性:

    • tree 是根据索引写入的任何内容
    • parent 现在是 HEAD 中的任何内容(或多或少——在进行合并提交时,与多个父级有一些摆弄)
    • authorcommitter 这些日期取自当前时间和您的 git 配置 user.nameuser.email,或者取自参数 (--author) 或环境变量(如果这些设置为覆盖某些内容)
    • 消息是您在提交消息中编辑的任何内容,或作为-m 参数提供的任何内容。

    所以 git 写入了那个提交,它产生了一个新的、唯一的 SHA-1。然后它将 SHA-1 本身写入某处。

    分支和HEAD

    如果您“在分支master”上,正如git status 所说,这意味着文件.git/HEAD 包含文字字符串ref: refs/heads/master。这就是 git 所说的“间接引用”:只是说“去寻找另一个引用,这是名称”的引用。通常你在某个分支上,HEAD 是对该分支的间接引用。

    分支本身可以以多种不同的方式存储,但最简单的是.git 中的另一个文件,在本例中为文件.git/refs/heads/master。如果该文件存在并且您阅读了它,它将包含一个类似于5f95c9f850b19b368c43ae399cc831b17a26a5ac 的 SHA-1。这是当前的提交,也是 git 如何知道您“在”哪个提交,就像 ref: refs/heads/master 是 git 如何知道您在分支 master 上。

    为了进行 提交,git 如上所述写入提交,这会产生一个新的唯一 SHA-1。然后,由于你在分支 master,git 只是将新的提交 ID 写入 .git/refs/heads/master,现在你在新的提交上,这是分支 master 的尖端。

    你也可以有一个“分离的 HEAD”,尽管听起来像是法国大革命的东西——只是意味着HEAD 不是间接引用。相反,HEAD 包含原始 SHA-1。在这种情况下,为了进行新的提交,git 以与以前相同的方式进行提交,但它不会更新 .git/refs/heads/master,而是将新的提交 ID 直接写入 HEAD

    git 重置

    所以,考虑到所有这些,让我们具体看看git reset 做了什么。

    如果您执行--soft 重置,git 将完全保持索引不变。这意味着它只更新当前分支。

    要更新当前分支,git 做的事情与进行新提交时相同:它找到 HEAD 间接指向的分支,并将新的 SHA-1 写入该引用。如果HEAD指向master,则只需将新的SHA-1写入.git/refs/heads/master即可。

    git 写入的 SHA-1 是你在命令行中提供的:

    git reset --soft @~   # @~ means @~1, which means HEAD~1, aka HEAD^
    

    您可以通过运行 git rev-parse 来查看 SHA-1 的内容(对于 HEAD-relative ref,您必须在 reset 更改 HEAD 之前执行此操作当然):

    $ git rev-parse @~
    9c8ce7397bac108f83d77dfd96786edb28937511
    

    如果您告诉git reset 使用--mixed,它也会更新索引。它放入索引的东西来自它将写入分支的提交 SHA-1:

    $ git reset --mixed HEAD -- COPYING
    

    在这里,通过告诉它将HEAD 更改为HEAD,您将重置为将分支与原来的位置完全没有距离,因此分支根本不会更新;但是-- COPYING 说“从目标修订版HEAD 中提取文件COPYING 的SHA-1,并将该SHA-1 放入文件COPYING 的索引中。”所以这意味着下一次提交不会对文件 COPYING 进行更改,因为我们已将旧的 SHA-1 放回索引中。

    如果您告诉git reset 使用--hard,它也会更新工作目录(它已经在更新分支和索引)。它通过从存储库中获取实际文件(或多个文件)内容(从唯一的 blob SHA-1 查找它们)并覆盖工作目录版本来实现此目的。如果您还没有 git add-ed 和 git commit-ed 那些工作目录版本,这意味着更改已经消失。 (如果你做了git add,它们在存储库中,但如果你没有做过git commit,它们有资格进行垃圾收集——见脚注。)

    由于您使用了--soft,因此您抑制了对索引的更改,所以git reset 唯一能做的就是更改分支提示文件.git/refs/heads/master 的内容。


    1git reset 曾经只有这三种操作模式。它现在有--merge--keep,加上--patch,比简单的情况做得更多。这有点像关于西班牙宗教裁判所的巨蟒短剧:“我们的三种模式是软、混合、硬和合并。...四种!我们的四种模式是软、混合、硬、合并、保持……”

    2存储库中的对象“永远存在”,但有一个非常大的例外:一个 未引用 对象,git fsck 显示为 dangling,是候选对象用于垃圾收集。未引用的 blob、提交等是完全正常的。它们占据着磁盘空间(通常很少:对象被压缩存储),以便您可以恢复事物,并且当 git 认为清理是个好主意时,它们可以被立即收集和丢弃。

    当某些外部标签(分支名称、标签、HEAD 或其他任何东西)直接或间接指向它们时,对象被“引用”(因此永远存在)。分支名称指向该分支上最尖端的提交。该提交指向它的树,它指向任何子树和 blob,因此所有这些都永远存在;并且该提交指向其父提交,因此这些父提交永远存在。每个父级提交点依次指向自己的父级,这些也永远保留。

    当您将分支标签移开时,提交将变为 un 引用:

    A <- B <- C   <-- HEAD=master
    

    这里master(我们当前的分支)指向CC 指向BB 指向A。但如果我们:

    $ git reset --hard HEAD^
    

    我们使master 指向B,它指向A。提交C 现在未引用:它已被废弃,最终将被垃圾收集,连同它的树和任何子树和 blob。类似的事件发生在,例如,git commit --amend,它执行软重置和新提交,创建指向 B 的新提交 D,并让 master 指向 D:

    A - B - D   <-- HEAD=master
          \
            C   [abandoned]
    

    rebase 操作复制然后放弃整个提交序列,生成大量候选对象用于垃圾回收。这就是为什么悬挂物体是正常的。

    【讨论】:

    • 我想我没有清楚地说明我的问题。我的问题是在我使用 git reset --soft 之前,我有一些文件已经在索引(暂存区域)中,并且在我使用 git reset --soft 之后。更多文件被添加到索引中。显然索引根本没有被触及。那么现在如何更改索引而不被触及?
    • 不是(已更改)。索引 still 包含“下一次提交”。如果您的意思是“为什么git status 的输出不同”,那是因为git status 将当前(HEAD)提交与索引进行比较。 (也针对工作树,但我们暂时忽略它...)您更改了HEAD 提交,因此比较结果不同。
    • 谢谢。所以索引已经被分支签出后最新提交的文件填充。我猜这是我没有清楚理解的部分。并且git add 将更多文件添加到索引文件中。我使用git reset 移动了HEAD,因此增量现在变得可见。但是如果索引已经有来自最新提交的那些文件。我还有一个问题,git如何快速使用索引来创建下一个提交? Git应该首先将它与最新提交进行比较,以便将最新提交中的文件排除在下一次提交之外。再次感谢。
    • 啊,不,这就是 git 与大多数其他修订系统不同的地方:一个新的提交存储 每个文件。但是,如果 SHA-1 未更改,则它不是每个文件的另一个 副本,而是 相同 副本。请记住,索引包含 SHA-1(对于每个文件)。如果您使用相同的索引 (git commit --allow-empty) 进行新提交,则新提交的 tree 与上一次提交的树相同。唯一需要的额外空间是新提交本身。对于--allow-empty,不需要也不需要进行比较。只有在被要求时才会进行比较,看看有什么不同。
    • 我现在明白了。作记录。 git ls-files -s 显示舞台区域中的内容。它确实是一个快照,包含所有提交或修改的文件。因此 git status 会将其与 HEAD 进行比较以显示增量。在 git reset --soft 之后 delta 改变了,只是因为 HEAD 改变了。感谢您的宝贵时间。
    【解决方案2】:

    索引文件是纯元数据,显示已暂存的内容及其文件系统统计信息。当您执行 git checkout 时,索引会从提交的树中加载——它具有所有这些文件的 SHA 以及写入工作树的文件系统大小/时间。

    当您暂存 aka track aka add content 时,该内容将被放入 repo 并将其 SHA 写入索引文件。

    git reset --soft 不会触及任何这些,它只会改变 HEAD 指向的内容。所以git status 所要做的就是告诉你索引与HEAD 提交或工作树的不同之处。

    【讨论】:

    • 所以当我在索引中有一些文件时(git add),实际上索引文件中有更多文件(来自当前分支的最后一次提交的文件和树)。在我git reset it 之后。通过与HEAD^ 比较,上次提交的那些文件成为可见的增量。对吗?
    • 就是这样 - 索引中没有树,它是一个简单的路径名列表,但除此之外你已经得到它。
    猜你喜欢
    • 1970-01-01
    • 2018-08-27
    • 1970-01-01
    • 2020-06-22
    • 1970-01-01
    • 2019-03-11
    • 2013-07-19
    • 2014-08-25
    相关资源
    最近更新 更多