On `git checkout stash@{0} -- .`

5
如果我运行其中一个(例如)
git checkout stash@{0} -- .

任何相对于索引修改的已存储文件都将显示为已暂存。这里是一个快速示例:

% git init demo
Initialized empty Git repository in /tmp/demo/.git/
% cd demo
% date >> file.txt
% git add file.txt
% git commit --allow-empty-message -m ''
[master (root-commit) e46cee5] 
 1 file changed, 1 insertion(+)
 create mode 100644 file.txt
% date >> file.txt
% git stash
Saved working directory and index state WIP on master: e46cee5 
HEAD is now at e46cee5 
% git checkout stash@{0} -- .
% git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   file.txt

这种行为让我感到惊讶。

我能够想到两种可能的解释方向(虽然我无法填补细节):

  1. 该行为仅仅是一种标准、通用的 git 行为的特定示例;
  2. 该行为是可取的,因此作为一个功能被故意添加到 git 中。

我的希望是答案是(1)。在这种情况下,我想知道更了解 git 设计的人如何能够“从第一原则”推导出上述行为(事前)。是否存在其他类似的 git 子命令会将文件作为“已暂存”签出的示例?

如果答案是(2),我想知道为什么这种行为(将文件作为“已暂存”签出)被认为是可取的。

如果既不是(1)也不是(2),我想知道什么才是实际发生的。

1个回答

8

适用于情况1,但要理解为什么,您需要了解存储内部的存储方式。如果按照预期使用git stash,您不需要知道这一点:也就是说,我怀疑没有人会想到用户会执行git checkout stash -- .。(请注意,stash@{0}在很大程度上只是写成stash的花式方法。)

提交相关信息

首先,请记住提交是快照加上一些元数据。我们不会在此处讨论元数据,但快照包含每个文件的副本。

这些快照通常从索引中制作。索引是Git的内部组件,主要存储为名为.git/index的文件,具有多个功能,但主要功能是它是您构建下一个提交的地方。它开始保存从当前提交中获取的每个文件的副本。也就是说,您运行:

git checkout master

或类似的命令会从名为master的提交所标识的冻结、Git化的每个文件的副本中填充索引。它还会在你的工作树中填充每个文件的可用(解冻和再水化,普通日常格式)副本。因此,在执行git checkout master之后,你将拥有每个文件的三个活动副本。
例如,假设你有一个名为README.md的文件,并且刚刚执行了git checkout。现在有三个活动的README.md副本。其中两个处于特殊的Git-only格式中,需要使用Git命令才能查看它们:
  • git show HEAD:README.md将显示README.md的冻结HEAD副本;
  • git show :README.md将显示README.md的索引副本;
  • README.md是你的工作树中的普通文件。
你可以随时替换索引中的副本。只需编辑工作树中的副本,以便您可以查看并处理它,然后运行“git add README.md”。这将覆盖旧的索引副本1,现在索引和工作树副本匹配,除了索引副本是Git在提交中使用的特殊准备冻结、脱水形式。由于您对其进行了更改,因此它不再与冻结的HEAD副本匹配(您无法更改)。
如果您还有其他七个文件,则现在您的索引中有八个文件。其中七个与HEAD中的副本相同; README.md不同。如果现在运行git commit,Git将把索引中的所有八个文件打包成一个新的提交。这个新提交成为当前(HEAD)提交,现在所有24个文件的副本 - 3个README.md文件和7个其他文件的21个副本,在HEAD、索引和工作树中 - 再次以通常的方式匹配。
我们称之为索引,因为.git/index是一个特殊的、突出的索引。Git确实有使用其他临时索引文件的能力,我们将会看到。2

1从技术上讲,文件的冻结Git化副本直接进入存储库作为,然后索引只是引用它。除了速度和Git自身的便利性之外,其主要影响与将文件的整个内容塞入索引中的效果相同。

2如果您使用git worktree add,则会添加一个工作树+索引对,并且添加的工作树的"the"索引位于不同的位置。实际上,您还获得了添加的工作树的其他私有HEAD。这个私有的HEAD不在.git/HEAD中,就像添加的工作树的主要索引不在.git/index中一样。


隐藏工作区的秘密

当你运行git stash save或者git stash push时,Git实际上会创建两个或者三个提交。我喜欢称其为藏匿包,因为Git利用每个提交的元数据的方式,但是这里我们主要需要讨论iw提交。如果你使用了--all或者--include-untracked标志,那么这个藏匿包中还会有第三个提交u

Git从你当前的索引中创建i提交。稍后的git stash apply或者任何使用apply的动词,将仅在你告诉它时使用此单独的i提交;否则它会完全丢弃它。我称之为i提交,因为它保存了你(主)索引的状态。

Git使用临时索引进行w提交。Git需要这个临时索引,因为编写提交的唯一方法是使用某个索引。它使用临时索引来避免干扰主索引,至少在这个阶段是如此。实际上,Git将您的主索引复制到这个新的临时索引中,然后对所有在临时索引中的文件运行git add,以便它们从工作树中得到更新。3然后,Git仅使用这个临时索引而不是常规索引进行提交。新提交看起来几乎像任何其他提交。

这个w提交还有一个奇怪的地方:它有两个或三个父提交,而不是通常的一个。其中一个父提交是当前(HEAD)提交。另一个是i提交。如果存在,则第三个父提交是提交u,否则,w是一个双亲提交。

一个两个或三个父级的提交,根据定义是一个合并提交。w 提交不是运行 git merge 的结果。这意味着对提交 w 进行 git show 很少有用:在这种情况下,git show 有一个特殊模式用于合并提交,但它没有任何用处。4这就是为什么 git stash 有一个 show 子命令的原因:git stash show 知道如何以更有用的方式显示 w 提交,通过直接将其与创建时的 HEAD 提交进行差异比较。

加上更多关于 git checkout 的了解,这将帮助我们回答您的最后一个问题。


3出于效率考虑,并且因为git stash push允许您提供路径规范,这并不是实际的工作方式。但在涉及所有疯狂的边缘情况之前,它是一个有用的起点心理模型。

4在我看来,git show对于普通合并提交的有用性也是有争议的,但那是另一个完全不同的话题。


git checkout很复杂

让我们稍微看一下,然后更详细地解释索引和git checkout。这尤其有用,因为Git 2.23引入了两个新命令:git switchgit restore

还有其他类似的git子命令可以将文件作为“staged”进行检出吗?

这里的单词stagedgit status说的。 我们已经注意到上面的索引 - Git也称之为暂存区 - 包含每个文件的副本,并且每个文件有三个活动副本。 让我们回到README.md案例,并将另一个名为main.py的文件添加到我们的列表中。

假设所有三个README.md文件彼此匹配,并且所有三个main.py文件也匹配(并且没有其他文件,或者它们也都匹配)。 运行git status将完全不会提及这些文件。这是因为git status运行了两个独立的比较:
  • 首先,git statusHEAD与索引进行比较。对于每个不同的文件,它会说staged for commit。对于相同的每个文件,它什么也不说。
  • 然后,git status比较索引和工作树之间的差异。 对于每个不同的文件,它会说not staged for commit。对于相同的每个文件,它什么也不说。

由于所有三个 README.md 的副本都匹配,并且所有三个 main.py 的副本也匹配,git status 不会显示有关它们的任何信息。但是,如果我们更改了这两个文件的工作树副本,然后运行 git add README.md,那么现在就会出现:

    HEAD            index         work-tree
-------------   -------------   -------------
README.md (1)   README.md (2)   README.md (2)
main.py (1)     main.py (1)     main.py (2)

这里括号中的数字表示哪个版本是哪个:版本1是提交时的版本,版本2是我们更新后的版本。
由于“HEAD:README.md”与“:README.md”不匹配,“git status”将其称为“已暂存以备提交”。但索引和工作树副本确实匹配。同时,“HEAD:main.py”和“:main.py”匹配,因此“git status”不会将其称为“已暂存以备提交”,但是索引和工作树版本不匹配,因此它将其称为“未暂存以备提交”。
如果我们再次触摸“README.md”的工作树副本,使其成为第3个版本,既不与版本1也不与版本2匹配,会发生什么?预测“git status”会说什么,然后“尝试一下”。
这也让我们回到了git checkoutgit checkout命令非常复杂。 它可以做4或5件不同的事情。这几乎肯定太多了,在Git 2.23中,Git团队引入了git switch,它只做一件事情(或者可能是两件),以及git restore,它也只做一件事情(或者可能是2或3件)。当然,老旧的git checkout仍然存在并且仍然做所有事情。
我在上面提到过这点,但现在让我们强调一下:当git checkout从一个分支切换到另一个分支时,它实际上会将文件从新提交的内容复制到索引中。它也将它们复制到工作树中。它执行此操作的精确方式以及在某些情况下何时以及如何不执行此操作变得非常疯狂,但如果您使用以下语法:
git checkout <tree-ish> -- <pathspec>

你需要告诉Git无条件地删除可能仅出现在索引和/或工作树中的未提交数据:它应该找到你在pathspec参数中列出的文件,作为tree-ish参数中存在的文件,并将它们复制到目前索引和工作树中的任何内容上。
结果是你的任何未提交的工作都被丢弃了。原本在你的索引和/或工作树中的东西现在被覆盖了,如果那些东西——文件数据——没有保存在其他地方,那么它现在真正消失了。6但无论你是否失去了什么,现在索引和工作树的副本与你选择的tree-ish的副本匹配。如果tree-ish对你来说没有任何意义,请继续阅读下一节。

5请参考在当前分支有未提交的更改时检出另一个分支这个最复杂的情况,但请注意git checkout不仅限于这种复杂情况。

6你的操作系统可能有一些方法可以从某些由操作系统提供的快照中获取它。例如,在Mac上,您可能会定期备份Time Machine。 这里的重点是Git无法再帮助您了。


tree-ish、提交对象和分支名称简介

Git的主要存储单元是提交对象。Git的全部内容都围绕着提交对象展开:当你进行一次提交时,你会冻结文件的快照,这个快照会永久地保存下来,或者至少在该提交对象存在的时间内保留。每个提交对象都有它自己独特的哈希值,这是一个由字母和数字组成的长字符串,在git log命令运行时可以显示出来。

实际上,文件保存在被Git称为树对象的结构中。提交对象本身及其哈希值表示提交对象,它其实相对较小,只包含元数据信息。快照本身存储在其中一个或多个树对象下,这些树对象也有哈希值;提交元数据提供了顶层树对象的哈希值。当你需要从提交对象中获取文件时,Git并不需要提交元数据,只需要树对象即可。因此你可以给它一个提交对象的哈希值,它就可以从其中找到树对象;或者你也可以给它一个树对象的哈希值。

很少有理由去打扰树对象,但是“树状”的还是很有用的,因为索引对于Git的许多内部部分的工作方式都很像树。因此,需要树状物的许多内部位置也可以在(或者说一个)索引上工作。这里没有保证,但是一般来说,如果一个Git命令在树上工作,那么可能有一些变体可以在索引上工作。对于git checkout,就是git checkout-index7同样地,git diff主要比较两个提交或者真正的两个树,所以有一个git diff-index可以使用索引。8

与此同时,像masterdevelop这样的分支名称实际上具有多个功能。其中之一是特定于git checkout:您可以git checkout master来获得on branch master,就像git status所说的那样。在执行git checkout develop后,您将处于on branch develop状态。但另一个功能是每个分支名称都标识了一个特定的提交。因此,名称master代表着一些长长的、丑陋的哈希值。
您可以使用git rev-parse查找任何分支名称的哈希值:
$ git rev-parse master
7c20df84bd21ec0215358381844274fa10515017

在这种情况下,运行git checkout master时得到的提交7c20df84bd21ec0215358381844274fa10515017
任何给定的提交都可以有零个、一个、两个或多个分支名称。它也可以有零个或多个标签名称。其他名称,例如远程跟踪名称,可以并且确实是指特定的提交。但是,像master这样的分支名称的一个特殊功能是它会随着时间的推移而发生变化,实际上,每当您进行新提交时,它就会自动更改
这就是所谓的“在分支上”。如果你在“master”分支上进行了一个新的提交,那么这个新的提交会得到一个新的、独特的、又臭又长的哈希ID——现在,“master”这个名字指的就是你刚刚创建的那个新提交。随着你做更多的提交,每个新的提交都成为“master”所指的提交。这就是分支的增长方式:你做新的提交。这也是每个提交中父级元数据的作用,但我们不会在这里详细介绍。
无论如何,“master”这样的名称意味着一个特定的提交。如果你将这个名称给“git checkout”,Git 将尝试检出那个特定的提交,并且将你放在那个分支上,以便新的提交将更新“master”这个名称。但是你也可以在其他地方使用这个名称来表示“那个提交”。
名称stash——它的实际全名是refs/stash,以区别于任何分支名称9——同样只指向一个特定的提交。在这种情况下,它指向当前存储中的w提交。
名称的一般形式都以refs/开头,都是引用。分支名称是refs/heads/*,标签名称是refs/tags/*等等。分支名称的特殊之处在于,它们会自动移动,并且git checkout可以让你"进入"它们。你可以git checkout其他名称;结果是Git称之为游离HEAD(detached HEAD),位于由名称标识的提交上。

形如ref@{number}的名称,例如stash@{1}master@{3},使用Git所谓的reflogs。 Reflogs主要存储引用的先前值。 git stash代码使用-有些人可能会说滥用-refs/stash的reflog作为一种堆栈:将当前重新编号的弹出(或删除)stash@{2}stash@{1}stash@{1}stash@{0}。 创建新的stash“推入”它到stash@{0},将所有其他数字向上移动一步。

你可以用相同的方式处理其他引用日志,比如那些针对 master 的日志,但这不是它们的预期使用方式。相反,每次更新只会递增所有现有数字:新建两个提交,之前的 master@{0}(或仅为 master)现在变成了 master@{2}。使用 git reset 删除其中的最后一个,现在它已经移到了 master@{3}master@{1} 保存了你刚刚通过 git reset 放弃的提交。

大多数 Git 命令:

  • 操作索引和/或工作树,和/或
  • 使用或扩展提交图形(参见像 Git 一样思考),和/或
  • 操作引用及其引用日志。

git reset 命令可以执行三个操作;git commit 使用索引来添加提交和更新当前分支名称来执行第二个和第三个操作。 git merge-base 命令使用图形查找一个特别有趣的提交,而不更改索引或工作树并且不修改任何引用。 一些 Git 命令——git fetchgit push——让你的 Git 调用其他 Git,并传递或接收提交和其他 Git 对象,然后可选择修改自己的引用(git fetch)或要求它们修改它们的引用(git push)。


7实际上,git checkout-index的功能大部分已经包含在git checkout中了。这确实是一个具有过多操作模式的命令。

8与checkout一样,git diff也可以直接执行此操作。但在这种情况下,git diff是一个面向用户的porcelain命令,有三个底层的plumbing命令:git diff-treegit diff-indexgit diff-files。当编写脚本时,应使用管道命令,因为瓷器命令具有用户配置设置,使它们对不同的用户工作方式不同。脚本主要需要可预测的行为:你的脚本被某人的diff.renames设置或颜色选项所绊倒是不好的。

9分支名称以refs/heads/开头,因此如果您有一个名为stash分支,则它将是refs/heads/stash,这与refs/stash明显不同。虽然Git本身可以区分这一点,但这是一个坏主意:不要这样做。人们会感到困惑,并且不知道stash是指refs/stash还是refs/heads/stash


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接