检查另一个分支 x 中未提交的更改,而不检查分支 x

Check for uncommitted changes in another branch x, without checking out branch x

我想确定另一个分支是否有未提交的更改,但我不想检查那个分支。基本上我想知道另一个分支是否有 "dirty index"。这意味着未提交的暂存或未暂存更改。

这可能吗?据我所知,我不认为你可以在你当前签出的分支之外的任何分支上使用 git status。但是 git diff 呢?

更新:我刚刚意识到唯一可以有 "dirty index" 的分支是你当前的分支,除非你使用 git-stash(我思考)。 所以也许这个问题更好地表述为 - 我如何检查任何分支是否有 stashed/uncommitted 更改?

注意:这花了一些时间来写,同时你更新了你的问题。这个答案比它可能需要的要长得多。


I want to determine if another branch [i.e., a branch other than the current branch] has uncommitted changes ...

答案是没有。

事实上,no 分支包含未提交的更改。这实际上是不可能的,问这个问题意味着您认为 Git 以其他方式工作,而不是 Git 实际上确实有效。

Basically I want to find out if another branch has a "dirty index".

只有一个索引。好吧,这不是 完全 正确的,但从那开始吧!三也只有一个 work-tree(这也不 相当 正确,但同样,让我们​​从那个开始)。

... [update] So perhaps this question is better formulated as - how can I check if any branch has stashed/uncommitted changes?

在这里,您可能希望使用$(git --exec-path)/git-sh-setup中的辅助例程。例如,在您最喜欢的编辑器中查看此文件。

Git 是如何工作的

Git 的实际工作方式有点复杂。任何 Git 存储库的主要 body 是一个包含四种类型 Git object 的数据库,其中对我们来说最重要的是 提交。我们可以将每个 Git 存储库视为由一系列提交组成。每个提交都有一个唯一的哈希 ID——像 b7bd9486b055c3f967a870311e704e3bb0654e4f 这样丑陋的十六进制数字之一——Git 用来提取提交 object 的内容。 object 本身非常小,但包含对其他哈希 ID 的引用,这些哈希 ID 最终会在该提交的快照中为您提供所有 文件

与此同时,像 master 这样的分支名称只包含哈希 ID。每个分支名称都包含 一个 哈希 ID。所以像 master 这样的名字拥有像 b7bd9486b055c3f967a870311e704e3bb0654e4f 这样的 ID。 Git 将此称为该分支的 提示提交 。 name-to-hash-ID 映射是第二个数据库,位于主 hash-ID-to-object 数据库旁边。

提交本身包含另一个哈希 ID——好吧,不止一个,但让我们看一下这个提交。 git cat-file -p 命令 pretty-prints 一个 object.

的内容
$ git cat-file -p b7bd9486b055c3f967a870311e704e3bb0654e4f | sed 's/@/ /'
tree 1fd4a47af4942cbdee0bdcb4375612ab521a4a51
parent 5571d085b3c9c2aa9470a10bcf2b8518d3e4ec99
author Junio C Hamano <gitster pobox.com> 1531941857 -0700
committer Junio C Hamano <gitster pobox.com> 1531941857 -0700

Third batch for 2.19 cycle

Signed-off-by: Junio C Hamano <gitster pobox.com>

tree 行为我们提供了该提交的快照。 parent 行告诉我们哪个提交在之前 提交。其余行包含剩余的元数据,包括提交人的姓名和日志消息。

因为这个东西在提交中,所以都提交了。也没有任何 更改 :此处列出的 tree 包含 所有 文件的完整快照。我们可以直接查看树,再次使用 git cat-file -p,但是它又长又有点无聊:

100644 blob 12a89f95f993546888410613458c9385b16f0108    .clang-format
100644 blob 1bdc91e282c5393c527b3902a208227c19971b84    .gitattributes
[snippage]
100644 blob 536e55524db72bd2acf175208aef4f3dfc148d42    COPYING
040000 tree 814822a9a0a75e17294704b37950c30361401a85    Documentation
[lots more snippage]
100644 blob ec6e574e4aa07414b9a17bb99ddee26fd44497de    xdiff-interface.c
100644 blob 135fc05d72e8f066a63902785d12485a656efa97    xdiff-interface.h
040000 tree 12edaa3d770f84e31ee58826eea93ea6ca64d939    xdiff
100644 blob d594cba3fc9d82d94b9277e886f2bee265e552f6    zlib.c

如果我们遵循每个子 tree 行,我们最终会在 Git 的 Git 存储库中收集提交 1fd4a47af4942cbdee0bdcb4375612ab521a4a51 的整个快照。每个 blob 行都会为我们提供与该提交一起使用的文件的哈希 ID,而文件的 name 位于该行的末尾。所以 Git 中的这个提交有一个 zlib.c 的版本,它的散列 ID d594cba3fc9d82d94b9277e886f2bee265e552f6,如果需要,我们也可以使用 git cat-file -p 查看该文件:

$ git cat-file -p d594cba3fc9d82d94b9277e886f2bee265e552f6 | head
/*
 * zlib wrappers to make sure we don't silently miss errors
 * at init time.
 */
#include "cache.h"
[rest snipped]

这些都冻结了/read-only

存储在主存储库 body 中的所有内容,在这些哈希 ID 密钥下,完全是 100% read-only。原因很简单:哈希 ID 密钥实际上是数据的加密校验和! (好吧,数据加上一个非常小的 header 给出了 object 的类型和大小。)当你给 Git 一个散列 ID 作为键时,Git使用它从存储库数据库中提取 object,Git 进行一致性检查:它 re-checksums 检索到的数据以确保它与它首先使用的哈希 ID 匹配。这两个 必须 匹配:如果它们不匹配,Git 知道 object 已经以某种方式损坏,例如,由于磁盘故障,或其他原因行。

名称查找提示提交;提交找到他们的 parents,形成一个向后链

正如我们上面提到的,像 master 这样的名称包含该分支的 tip 提交的提交哈希 ID。如果我们使用哈希 ID 来检索提交本身,我们会得到 parent 行。 parent 告诉我们 分支顶端的提交的哈希 ID,在更早的时间。我们说这些东西中的每一个——分支名称和提交本身——指向一个提交,我们可以将这些指针画成箭头:

... <-parent  <-tip   <--master

Git 可以从 tip 提交开始,然后对该提交做一些事情。然后 Git 可以使用 parent ID 找到以前的提交,并用它做一些事情。 parent 是它自己的 parent,Git 可以查看该提交,依此类推。最终,整个链条会回到操作停止的点,通常是在第一次提交时。如果有两个分支名称,在它们的分支提示处有不同的提交,在一个非常小的存储库中,我们可以像这样画出整个东西:

A--B--C--D   <-- master
       \
        E--F--G   <-- develop

内部链接总是指向向后,从提交到 parent。由于 object 本身是 read-only,没有提交记得它的 children:它们创建得太晚了。但是所有 children 总是记得他们所有的 parent。当我们进行 merge commit 时,例如将 G 合并到 D,合并会记住它的 both 378=]s:

A--B--C--D------H   <-- master
       \       /
        E--F--G   <-- develop

并且每当我们进行新提交时都会发生同样的事情:当前分支名称发生变化,因此master指向H而不是 D。合并提交的 first parent,以及普通 non-merge 提交的唯一(也是第一个)parent,是 刚才的提示。

换句话说:分支名称移动;提交保持不变。

提交的所有内容都采用特殊的 Git-only 格式

blob objects,我们在上面看到的包含文件的东西,由哈希 ID 标识,在一个特殊的 Git-only, 压缩格式。 (提交和树也被压缩,尽管这不太重要,因为只有 Git 真正直接使用它们。)但这对我们自己的使用没有好处,所以我们需要一个地方 Git可以将文件提取为普通的未压缩格式。这些文件还需要 可更改,而 Git-only 冻结的 blob object 则不是。

因此,每个存储库通常带有一 (1) 个 work-tree,其中 Git 提取和解压缩已提交的文件。 work-tree 的初始内容来自冻结提交之一。

Git 当然需要一种方法来进行 new 提交。有一个类似的版本控制系统 (Mercurial) 使用 work-tree 进行新提交,Git 可以做到这一点,但 Git 不会那样做。 Git 取而代之的是,这个东西被称为 index,或 staging area,或 缓存。索引的作用可能会变得相当复杂——例如,Git 在有冲突的合并操作期间将其扩展很多——但最好将其描述为 构建下一次提交的位置。这是它作为暂存区的作用。

新提交是完整的快照,而不仅仅是更改!所以索引 / staging-area 包含 当前提交的所有文件 。 Git 简单地将当前提交 复制到 索引,这使得索引包含所有文件。索引中的文件仍然是特殊的 Git-only 格式,但是——这是与提交副本的关键区别——它们现在是 可写.

我们现在可以观察签出提交和进行新提交的过程

让我们从 six-commit 存储库开始。在此存储库中,让我们也添加 remote-tracking 名称 (origin/*),并将名称 HEAD 附加到 master

A--B--C--D   <-- master (HEAD), origin/master
       \
        E--F   <-- origin/develop

现在让我们这样做:

$ git checkout develop

(假设存储库刚刚被克隆并且拥有所有内容 "clean")。分支名称 develop 还不存在!而不是失败,git checkout 实际上 创建 它现在使用 origin/develop 作为哈希 ID,因此名称 develop 应运而生,指向提交 F.

下一步可能会非常复杂(参见 Checkout another branch when there are uncommitted changes on the current branch),但我们假设一切都是干净的,因此我们可以进一步简化它:Git 将树的内容放在 F,将其放入索引,并通过删除其中不应该存在的所有文件并使所有其他文件与索引中的文件匹配来使 work-tree 匹配,但 de-compressed. Git 将名称 HEAD 附加到 develop。我们的 commit-graph 图没有改变,除了 HEAD 附加到新的 name develop:

A--B--C--D   <-- master, origin/master
       \
        E--F   <-- develop (HEAD), origin/develop

我们现在可以愉快地修改work-tree中的文件了。完成后,我们 运行 git add 对我们改变的那些。这会将 work-tree 文件复制到索引中,压缩它们并为提交做好准备:

$ [edit various files]
$ git add -u             # or -A or `.` or list the files or whatever

现在索引匹配 work-tree,我们 运行:

$ git commit             # with -m to avoid using the editor, or whatever

Git 打包索引的内容(作为一个包含适当子树的树),将所有 pre-compressed 文件冻结到一个新树中,并进行新的提交。新提交的 parent 是 F,因为 HEAD 附加到 develop 并且 develop 包含 F 的哈希 ID。新提交的树就是刚刚打包的那棵树Git,作者是"us",依此类推。写出提交会为我们的新提交生成一个新的、唯一的哈希 ID G。 Git 将哈希 ID 写入名称 develop,现在我们有:

A--B--C--D   <-- master, origin/master
       \
        E--F--G   <-- develop (HEAD), origin/develop

我们的索引和 work-tree 相互匹配并匹配提交 G,因为 G 来自 我们的索引。

所有其他项目从这里构建

在编辑或评论中,您提到使用 git stash 编辑。 git stash 所做的是 提交 git stash 所做的提交的特别之处在于它们在 no 分支上。

它实际上至少进行了两次提交,一次用于当前索引内容,一次用于 work-tree 内容,以防你已经上演(使用 git add)一些东西和 not-staged(通过不添加)更多的东西。这两个提交都是完整的快照!我喜欢把它们画成 iw:

A--B--C--D   <-- master, origin/master
       \
        E--F--G   <-- develop (HEAD), origin/develop
              |\
              i-w   <-- refs/stash

Git 通过特殊名称 refs/stash 找到 w 提交(这不是分支名称——分支在 refs/heads/ 中,例如 refs/heads/masterrefs/heads/develop)。 w 提交找到了 i 提交,以及进行存储的提交 (G); i 提交还链接回您进行存储时的当前提交。

完成两个(正常存储)或三个(--include-untracked--all)存储提交,git stash 运行s git reset --hard 清理索引和 work-tree,使它们匹配当前提交。这里还有更多选项,但涵盖了 git stash.

的基本操作

git worktree add,以及其他特殊情况

使用 git worktree,我们可以创建新的、附加的 work-trees 与当前存储库一起使用。每个添加的 work-tree 都有自己的索引。每个 work-tree 缓存的索引(因此得名 cache)大量数据 about work-tree,以及 Git 使用它来快速扫描甚至避免扫描 work-tree:这就是索引 indexes 和 work-tree 的方式和原因,因此名称索引.

除此之外,您还可以创建自己的临时索引文件!例如,这就是 git stash 的工作原理。首先,它对 current 索引进行 mostly-ordinary 提交 i,这很容易,因为 Git 已经知道如何做到这一点,但随后它有提交 work-tree。 Git 只能从索引构建新提交,所以 git stash 所做的是创建一个 临时 索引,将所有 work-tree 文件塞入其中,并使用它来进行 w 提交。

许多其他奇特的 Git 技巧可以利用临时或备用索引文件。必须小心使用这些,因为 Git 通常假设 the 索引索引/缓存 the work-tree——或者添加的每个 work-tree 索引缓存特定添加的 work-tree——如果你让事情不同步,就会发生一些有趣的微妙故障。

工作树和索引不特定于任何分支。因此,"unstaged" 或 "uncommited" 更改不是您可以直接查询的分支的 属性。

但是,您可以将工作树与特定分支进行比较:

git diff <branch name>

或者将索引与特定分支进行比较,

git diff --cached <branch name>

在这两种情况下,省略 <branch name> 基本上默认为当前签出的分支。