为什么 git 日志的 --walk-refs (-g) 选项会禁用 --stat 和 --patch?

Why does the --walk-refs (-g) option to git log disable --stat and --patch?

this answer 开始,我发现与 git stash list 相比,通过使用 git log -g stashgit log --walk-reflogs refs/stash 的缩写)我可以获得更多的能量。例如,与 git stash list 不同,我可以添加选项来缩小影响一组文件或目录的存储:git log -g stash -- Dir1 Dir2.

不过,我发现无论我如何排列参数,我都无法让 -p/--patch--stat--walk-reflogs 一起工作。 git 日志的帮助并未表明这些选项有任何不兼容之处。我是否缺少让它工作的方法,或者是否有某些原因导致 --walk-reflogs 与检查补丁的属性不兼容?

TL;DR

并不是 -g 禁用了 它们,而是 git log 一开始就没有对隐藏提交做正确的事情。

考虑 运行使用 --format=%H 使用 git log 命令获取原始哈希 ID,然后 运行使用 第二个 在第一步中找到的每个哈希 ID 上的一组 Git 命令(例如 git stash show --stat)。 Git 是一组工具,而不是一个解决方案:每个工具都会产生,或者无论如何都可以产生输出,作为另一个工具的输入。

或者——请注意,这是一个特殊情况,它最多只使用了很少记录的内容——使用:

git log -m --first-parent -p -g stash

这样 -m --first-parent 使 -p 有效。这也适用于 --stat

从技术上讲,git stash 所做的提交是 合并提交

在正常的 git log 中,当 Git 遍历 提交图 而不是 reflogs 时, Git 正在查看 parent/child 每个提交的关系。例如,假设我们有:

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

我们运行 git log somebranch。 Git 首先从 b运行ch 名称中找到提交 H 的哈希 ID,这使它可以轻松访问提交 H 本身。 Git 加载 H 的元数据,其中包括早期提交 G 的哈希 ID,现在在内存中有两个哈希 ID,GH .

使用 -p--statgit log 将 运行 在这两个提交哈希 ID 上添加 git diff——GH—在内部显示结果差异,或来自该差异的统计数据。然后 Git 将继续提交 G 并显示 it,加载 G 的元数据,它产生早期提交的哈希 ID [=41] =].

您在每个点看到的差异或统计数据是当前提交 是单个父 的子项及其父提交与父提交之间的差异作为差异的左侧,当前提交作为右侧。然后 git log 继续显示父提交。所以差异有点“介于”提交和它的父级之间。这一切都非常有道理和逻辑。

然而,当遍历 reflogs 时,我们可能会遇到这样的事情:

...--G--H   <-- branch@{0}
         \
          I--J--K   <-- branch@{1}

例如,在丢弃提交 I-J-Kgit reset 之后就是这种情况。 git log 命令会将提交 H 显示为与提交 G 的差异,然后将提交 K 显示为与 J 的差异。只要您为此做好准备,并了解这里发生的事情,就没关系:这就是 git log 实际要做的。

但是我在上面用粗体写了一个短语,关于与单亲的提交。 git log 遇到合并提交时,它根本不会费心去做差异,至少在默认情况下是这样。也就是说,当提交图如下所示时:

...--I--J
         \
          M--N   <-- branch
         /
...--K--L

git log 到达提交 M 本身,它根本不显示 -p--stat 输出。它从 M 移动到 J,并且在提交 J 时,它 运行 差异(针对提交 I).它还从 M 移动到 L——除非你要求 --first-parent,也就是说——在 L,将显示差异(从 K).但是在 M 本身,它必须做 and/or 显示 两个 差异,并且......它不会打扰!

可以强制git log去解决这些差异,但有几个注意事项。最重要的是,这些差异通常旨在对合并提交做一些有用的事情,因此它们默认会省略很多信息。他们省略的信息通常完全破坏了他们对 git stash 的有用性,因为虽然 git stash 做出 的提交,从技术上讲,合并提交,这些合并提交没有一种有用的形式 作为 合并:当您稍后对这些提交使用 git stash 时,隐藏代码将它们分开,一次提交一个,而不是使用它们通常使用合并的方式。

藏品的形式

git stash 做出的提交采用两种形式之一。你要么得到这个:

...--o--o--C   <-- branch (HEAD)
           |\
           I-W   <-- stash

或:

...--o--o--C   <-- branch (HEAD)
           |\
           I-W   <-- stash
            /
           U

当您 运行 git stash savegit stash push 时,您的 当前提交 是提交 C。 Git 通过特殊名称 HEAD 找到它,该名称附加到您的 b运行ch 名称 branch,指向提交 C。 stash 命令现在构建两个 (I-W) 或三个提交,然后更新 refs/stash 以指向新提交 W。两个或三个提交具有这些属性:

  • I(索引)提交包含 Git 的 index 又名 暂存区中的任何内容当时你运行git stash.

    如果您使用 git addgit add -p 或其他更新 Git 索引的方法,以便 Git 中的文件 index/staging-area 与当时提交 C 中的文件不匹配ou 运行 git stash, commit I 会有一些有用的东西。事实上,如果您在 每个 文件上使用 git add,提交 I 将与提交 W 完全匹配!否则,如果您在 没有 个文件上使用 git add,提交 I 的内容将与提交 C 完全相同的文件集.无论哪种方式,由于 Git 的 de-duplication,任何共享内容都将被共享,因此不占用磁盘 space。但是无论如何,提交 I 仍然存在:这就是 git stash 知道 W 隐藏提交的方式。1

  • W (work-tree) 提交保存工作树中的任何内容,作为跟踪文件(Git 索引中存在的文件) , 当时你 运行 git stash.

    这意味着提交 W 具有大多数人通常认为的隐藏内容:他们尚未 staged-and-committed 修改的文件。它实际上有 所有 文件,包括 更改的文件,就像任何其他提交一样。 stash ref(或稍后的 reflog 条目)直接指向提交 W,因此当您 运行 git stash show stash@{2}git stash apply 时,就是这样git stash 找到提交 W,从中找到提交 I,如果存在,也找到提交 U

  • U 提交(如果存在)包含同时存在于您的工作树中的所有未跟踪文件(可能包括被忽略的文件)。此提交仅在您使用 -u-a 或其更长的拼写时存在。它 保存普通文件(来自提交 C 或您的 work-tree 或任何地方),这使它成为一个非常奇怪的提交。出于这个原因,它根本没有父提交:它是一个 root 提交,就像某人在一个新的空存储库中所做的第一次提交。

git stash 完成这两个或三个提交后,它会重置内容(使用 git reset --hard)。如果您进行了 U 提交,git stash 也会从您的工作树中删除存储在 U 提交中的所有文件。在这一点上对我们来说有趣的是——我们正在使用 git log——尽管如此,W 具有合并提交的 形式 ,但不是合并提交的标准 content:它不是通过合并其父项构建的,而是通过制作工作树文件的快照,其名称列在 Git' s 索引 / 在 I 提交中。2


1这是一个非常糟糕的测试,因为任何合并提交都会通过它,但总比没有好。

2从历史上看,索引中的“intent to add”标志存在错误。我没有检查它们是否在 Git 的任何特定版本中针对 git stash 进行了修复,但我会小心避免被它们绊倒:不要将 git stash 与 I-T-A 东西。好吧,更一般地说,我会说:根本不要使用 git stash,除了非常特殊的 short-term 情况。


强制 git log 差异合并

有三个选项可以使 git log 通过合并显示差异(或 diff-stat):

  • -m 告诉 git log “拆分”合并。

    此选项接受任何合并提交,并且出于差异目的,假装它是多个单独的提交,每个提交都有一个父级。每个虚拟 single-parent 提交都具有合并所具有的 snapshot,但只有合并的 N 个父项之一。因此,标准 two-parent 合并会产生两个差异,而 three-parent 合并会产生三个差异,依此类推。

    这,或者 运行宁 git diff 你自己手动,是 真正 看到合并中发生的事情的唯一方法。 对于真正的合并,您通常不会在意看到真正发生了什么,因为这些信息可能既多又[=281] =]不相关.

  • -c 选项生成 组合差异 .

  • --cc 选项(两个连字符和两个 cs)产生一个 密集组合差异(有时称为“压缩combined diff”,这使得 --cc 的拼写至少有意义。

组合差异选项很难描述,但两者都有一个关键元素,可以使它们在真正的合并中有用,但在存储中无用。请记住,合并提交有两个或多个父项。 combined diff 将合并提交的内容与每个父项的内容进行比较。如果合并快照确实使用父文件之一,该文件将在合并差异输出中完全省略

真正的合并,就是说:合并结果只是re-used一个b运行ch的文件批发。通常,在检查合并时,您并不关心这个文件:您只关心必须解决的 冲突结果文件不再匹配任一输入父 -c--cc 选项旨在向您显示 这些文件 .

但是通过隐藏,I 提交通常 完全匹配 CW 提交。如果它确实匹配 W 提交,这将 忽略每个文件 。如果 I 匹配 C 我们的状态更好,但是 -c--cc 选项在这里都是错误的方向。

最后,还有一个方便的 special-purpose 选项,--first-parent。此标志的主要功能是改变 Git 如何通过 遍历 合并,仅跟随第一个父级。但是,还有一个次要功能。请注意,此选项的操作最近已略有更新,因此 git log --first-parent -p 现在等同于 git log --first-parent -m -p3 但无论您的 Git vintage,您可以编写 git log --first-parent -mp 来调用次要功能。在这里,m 选项像往常一样“拆分”合并,但是 --first-parent 与此操作结合以 diff 仅针对第一个父 (以及步行使用常规图形遍历时只有第一个父级)。


3此功能是 Git 2.29.0 中的新增功能。要禁用它,请使用 git log -m -p --no-diff-merges.


把这些放在一起

最后这一切意味着,为了查看带有 git log -g 的存储,-m 选项是必需的,以便 (a) 启用 -p--stat 选项和 (b) 使生成的差异有用。 --first-parent 选项是可取的,因为如果没有它,即使 Git 遍历引用日志而不是提交图,每个存储将显示为两个(常规存储)或三个(-u-a 隐藏)差异。

如果您的 Git 版本是 2.29 或更高版本,您可以使用 git log --first-parent -p -g stash:现在隐含了 -m。或者,无论 Git 年份如何,您都可以使用 git log --first-parent -mpg,使用组合 single-letter 标志的能力。