`git stash show -p stash@{N}`和`git show stash@{N}`有什么区别?(涉及IT技术)

13
我认为它们应该基本相同,但当我尝试了一下后。
$ git stash show -p stash@{N}

$ git show stash@{N}

后者显示了一些附加的提交信息,但实际的差异要短得多。 (前者显示大约十几个文件,而后者仅显示一个文件。)

那么,这两者之间到底有什么区别,为什么它们不同?

我是否也可以依赖像 git diff stash@{M} stash@{N} 这样的东西是正确的?

2个回答

21

隐藏包

git stash 保存的东西是我称之为"隐藏包"的内容。它由两个1不同的提交组成: "索引" 提交(暂存区)和 "工作树" 提交。 工作树提交是一种有趣的合并提交。

让我在这里再画一遍(参见引用答案以获取更长的版本),只是为了正确说明。假设您只有一个分支,上面有三个提交:AC。您在该分支上进行了一些更改,然后运行 git stash save (或只是简单地运行 git stash)。这就是你得到的:

A - B - C     <-- HEAD=master
        |\
        i-w   <-- the "stash"

现在你可能会创建(或切换到)另一个分支,但为了说明问题,让我们假设你把那个存储留在那里,并在 master 上进行更多的“常规”提交:

A - B - C - D - E    <-- HEAD=master
        |\
        i-w   <-- stash

这里的重点是,“stash-bag”即索引和工作树提交对仍然挂在之前的提交上。提交是无法更改的,这也适用于stash-bag提交。
但是现在你通过在master分支上进行一些更改并再次运行git stash save来创建一个新的stash。
旧的stash-bag会发生什么?“引用名称”2 stash,现在指向新的stash-bag。但是旧的stash-bag提交仍然存在。它们现在需要一个“reflog”样式的名称,stash@{1}3。
无论如何,现在你拥有的是:
A - B - C - D - E     <-- HEAD=master
        |\      |\
        i-w     i-w   <-- stash
          .
           -------------- stash@{1}

(当您使用git stash drop时,stash脚本只是操作stash引用的reflog,以删除已删除的stash-bag的ID。这就是为什么所有“更高级”的stash都会重新编号。实际的stash-bag本身将在下一个git gc上进行垃圾回收。)
(下一部分是理解正在发生的事情的关键。)

每当git需要您命名特定提交时,您可以使用许多不同的方法。

(每个提交都有一个“真实名称”,即您看到的大而丑陋的SHA-1散列值,例如676699a0e0cdfd97521f3524c763222f1c30a094。您可以编写它。它总是表示相同的提交。提交永远无法更改,并且那是提交整个内容的加密哈希,因此如果该特定提交存在,则该值始终是其名称。)
这对人名来说不是一个好的名称,因此我们使用别名:例如分支和标签名称,以及相对名称,如HEADHEAD~2,以及类似于HEAD@{yesterday}master@{1}的reflog风格名称。(有一个命令git rev-parse,可以将这样的名称字符串转换为哈希值。试试看:运行git rev-parse HEADgit rev-parse stash等。Git中的大多数功能都使用git rev-parse或其更强大的兄弟git rev-list将名称转换为SHA-1值。)
(有关如何命名修订版本的完整描述,请参见gitrevisions。Git还使用SHA-1来进行更多操作,但在这里让我们只考虑提交。)

Git stash show、git show和git diff

好的,最后,我们可以开始讨论你的git showgit stash show,以及git diff等内容。首先处理git stash show,因为这是您应该使用的。此外,git stash子命令将验证您命名的提交或者通过stash引用查找到的提交是否“看起来”像一个存储,即这些有趣的合并提交之一。

如果您运行git stash show -p,git会向您显示差异(-patch)。但它到底在显示什么呢?

回到存储袋的图表中。每个存储袋都悬挂在一个特定的提交上。上面,“主”存储现在从提交E悬挂下来,而早期的stash@{1}存储则从C悬挂下来。

< p > git stash show -p的作用是将该stash工作树提交(w)与stash所挂靠的提交进行比较。4

当然,您也可以自己执行此操作。假设您想要比较挂在提交E下的stash中的w与名为master的分支名称指向的提交E进行比较。因此,您可以运行:git diff master stash。这里的名称stash指的是(当前的)stash提交w,而master指的是提交E,因此这将产生与git stash show -p stash完全相同的补丁。(如果您想将stash@{1}中的w与提交C进行比较,只需运行git diff,然后指定这两个提交名称即可。当然,使用git stash show -p stash@{1}更容易些。)5

那么,git show 呢?这个有点复杂。 git show 可以展示一个提交,你给它了一个 stash 引用(无论是 stash 本身还是其中一个 reflog 变种)。那是一个有效的提交标识符,并且它解析为一个 stash-bags 中的一个 w 工作树提交。但是,当 git show 看到一个 合并 提交时会有不同的行为。正如文档所说:

它还会以 git diff-tree --cc 生成的特殊格式呈现合并提交。

因此,git show stash@{1} 会显示一个“组合差异”,假设提交 w 是提交 Ci 的正常合并,生成 w。实际上它并不是一个正常的合并,虽然组合差异可能实际上是有用的,前提是你知道你正在查看什么。阅读 --ccgit diff-tree 下的文档以了解其详细信息,但我要注意的是,--cc 意味着包括这一部分:

...仅列出从所有父级修改的文件。

stash 的情况下,如果在运行 git stash 之前使用 git add 添加文件,使得 iw 的差异为空,则您在此处的输出中将看不到这些文件。

最后,如果你执行git diff stash@{M} stash@{N}:这只是要求git diff比较不同的work-tree提交。这意味着什么取决于你要比较什么,通常取决于存储区袋子的附加位置。

1实际上是两个或三个,但我会将其画成两个。使用git stash save(或普通的git stash,这意味着git stash save)可以得到两个提交。如果你添加-u-a选项以保存未跟踪或所有文件,则会得到三个提交。这会影响存储恢复,但不会影响git stash show命令的输出。

2"引用名称"只是一个名称,类似于分支或标签名称。有许多可能的引用名称形式。分支和标签只是具有特殊目的的名称。"远程分支"是这些引用的另一种形式,"stash"也是一个引用。

事实上,HEAD只是另一个引用,尽管它是一个非常特殊的引用。它非常重要,以至于如果你删除HEAD文件,git会认为你的仓库实际上不再是仓库。

除了一些特殊情况(如HEADORIG_HEADMERGE_HEAD等),所有引用都以字符串refs/开头。分支以refs/heads/开头,标签以refs/tags/开头,“远程分支”以refs/remotes/开头。换句话说,引用具有“命名空间”,通常以refs/开头,然后在此之下再加一个单词来标识它们所在的位置。
存储堆栈引用拼写为refs/stash(并停在那里,没有refs/stash/jimmy_kimmel或类似的东西)。

3实际上,这确实使用了引用日志。这意味着,除了“主要”存储在refs/stash中的暂存内容外,其他暂存内容将会可能过期。(幸运的是,如musiphil所述,自git 1.6.0以来,默认情况下这些内容不会过期;你必须配置它们的过期时间才能使其发生——这可能不是你想要的.)

4它聪明的做法,使用后缀^符号进行标记,已经在我的other answer中详细解释。

5如果你想查看这些存储包中的索引提交,该怎么办呢? 嗯,好问题! :-) 存储脚本没有一个好的答案。 看到这些的简单方法是使用“^2”后缀来命名每个存储的第二个父项,即“i”提交。 如果您有一个包含未跟踪或所有文件的第三个提交的存储,那么它就是第三个父项:提交“w”看起来像是一个三个父项的合并,而“stash ^ 3”可以获取第三个父项。 但是再次说明,“w”不是正常合并,所以很棘手。 可能最好的查看存储的所有部分的简单方法是将其转换为自己的单独分支,使用“git stash branch”。


5
非常详细的回答,感谢您抽出时间。 - Mike Monkiewicz
1
关于“除了refs/stash之外的其他隐藏储藏,都会过期,就像所有的reflog条目一样”:GIT v1.6.0 Release Notes中提到,“默认情况下,隐藏储藏条目永不过期。将[gc“refs/stash”]中的reflogexpire设置为合理值,以恢复传统的自动过期行为”。 - musiphil
@musiphil:啊,好的,知道了。在更新的git版本中,还有其他引用的新控件,但出于某种原因,我错过了这个。 - torek
@torek:过去,我曾因git stash pop无法保留我的索引而感到沮丧。阅读了您的答案中的细节,尤其是关于有两个提交存储的事实,我更加仔细地查看了隐藏修补程序。我“发现”了git stash pop --index,它“尝试恢复的不仅是工作树的更改,还包括索引的更改”。要是我早点意识到这一点就好了。感谢您指出正确的方向。 - Rhubbarb
@torek:除此之外,我尝试使用“gitk --all”查看了一个包括未跟踪的 git stash save --include-untracked (-u),在其中我设置了所有四种组合的索引和非索引修改以及添加的文件。 在这种情况下,我发现似乎存储了 3 () 个提交:这种情况下的额外提交包含未跟踪的文件。 - Rhubbarb
显示剩余5条评论

2
我认为这是由于git将工作目录和索引分开存储的怪异行为所致。git stash show -p stash@{N}将显示存储中的所有更改,包括添加到暂存区的更改。但是,git show stash@{N}不会包括在存储之前已经暂存的更改。似乎git stash命令足够聪明,可以将它们合并在一起,而git show只是显示blob stash@{0}的内容。
是的,git diff stash@{M} stash@{N}将按预期工作。

1
git stash show -p仅查看工作目录版本,完全忽略索引版本。在大多数存储情况下,这并不重要,但是如果您添加了一堆内容,然后大部分撤消工作树副本,则来自git stash show的输出可能会非常误导人。这是存储脚本中我不太满意的东西之一。(但如果有明显的修复方法,那么无疑它们已经存在其中。:-))(另外我说“大部分还原”,因为如果您将工作树版本与HEAD完全同步,就会遇到“stash”的错误). - torek
我很感激另一个回答中提供的详细解释,但是为了快速了解,这个回答给了我关于暂存效果在分阶段和不分阶段之间的主要区别方面所需的信息。谢谢!(我假设这是一个准确的解释?是的……?) - redfox05

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