适用于情况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利用每个提交的元数据的方式,但是这里我们主要需要讨论i
和w
提交。如果你使用了--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 switch
和git restore
。
还有其他类似的git子命令可以将文件作为“staged”进行检出吗?
这里的单词staged
是git status
说的。 我们已经注意到上面的索引 - Git也称之为暂存区 - 包含每个文件的副本,并且每个文件有三个活动副本。 让我们回到README.md
案例,并将另一个名为main.py
的文件添加到我们的列表中。
假设所有三个
README.md
文件彼此匹配,并且所有三个
main.py
文件也匹配(并且没有其他文件,或者它们也都匹配)。 运行
git status
将完全不会提及这些文件。这是因为
git status
运行了两个独立的比较:
- 首先,
git status
将HEAD
与索引进行比较。对于每个不同的文件,它会说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 checkout
。
git 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-index
。7同样地,git diff
主要比较两个提交或者真正的两个树,所以有一个git diff-index
可以使用索引。8
与此同时,像
master
或
develop
这样的
分支名称实际上具有多个功能。其中之一是特定于
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 fetch
和 git push
——让你的 Git 调用其他 Git,并传递或接收提交和其他 Git 对象,然后可选择修改自己的引用(git fetch
)或要求它们修改它们的引用(git push
)。
7实际上,git checkout-index
的功能大部分已经包含在git checkout
中了。这确实是一个具有过多操作模式的命令。
8与checkout一样,git diff
也可以直接执行此操作。但在这种情况下,git diff
是一个面向用户的porcelain命令,有三个底层的plumbing命令:git diff-tree
、git diff-index
和git diff-files
。当编写脚本时,应使用管道命令,因为瓷器命令具有用户配置设置,使它们对不同的用户工作方式不同。脚本主要需要可预测的行为:你的脚本被某人的diff.renames
设置或颜色选项所绊倒是不好的。
9分支名称以refs/heads/
开头,因此如果您有一个名为stash
的分支,则它将是refs/heads/stash
,这与refs/stash
明显不同。虽然Git本身可以区分这一点,但这是一个坏主意:不要这样做。人们会感到困惑,并且不知道stash
是指refs/stash
还是refs/heads/stash
。