首页 > 解决方案 > 将所有新文件从暂存区移动到单行中的工作区

问题描述

是否有一个优雅的命令将 HEAD 所有文件仅从暂存区(待提交列表)重置回工作区?这发生在我做了git add -- '*.py'. 一个相关的问题是如何在上述语法中只添加对现有文件的更改。

标签: git

解决方案


TL;博士

有一些单行 shell 命令可以执行我认为您想要的操作,您可以将其形成脚本或 Git 别名。有关详细信息以及一些注意事项和改进,请参阅长答案。其中最直接的假设您具有 Unix/Linux 风格xargs

git diff --cached --name-only -z --diff-filter=A | xargs -0 git reset HEAD --

这很容易适应其他状态字母。

让我们从这个开始:

  • 每个提交都将每个文件存储为快照。

  • 更准确地说,它存储索引/暂存区域中的副本(这两个名称指的是同一个 Git 实体)。

  • git status命令运行两个内部git diff --name-status操作。其中第一个将HEAD提交中存储的所有文件与 Git 索引中存储的所有文件进行比较。

因此,如果file-new.ext在索引中命名的文件被认为是Added(“新”),则它不能存在于HEAD. 这反过来意味着:

git reset HEAD -- file-new.ext

只会从 Git 的索引中删除副本。file.ext同时,如果一个名为的文件file-old.ext被认为被M修改,它必须同时存在于HEADGit 的索引中。这反过来意味着:

git reset HEAD -- file-old.ext

将提交的版本复制file-old.ext回 Git 的索引。

考虑到这些,让我们首先重新审视标题中问题的措辞:

将所有新文件从暂存区移动到单行中的工作区

有问题的两个示例文件 (file-new.extfile-old.ext) 可能已经存在也可能不存在于工作树中。它们肯定存在于 Git 的索引中。如果我们要将它们从 Git 的索引移动到工作树,这可能会覆盖这些文件中出现的任何不同的工作树数据,如果它们现在在工作树中的话。但是移动它们并不是您的问题主体所假设的:

重置 HEAD

我假设这意味着运行git reset HEAD,或者做一些等效的事情,正如我们刚刚提到的那样,这实际上意味着HEAD提交复制到索引/暂存区域,这可能涉及从索引/暂存区域中删除

因为git reset HEAD它只是我们想要的——从HEAD索引复制,或者如果文件没有出现在索引中删除HEAD——并且可以在每个文件的基础上运行,它可能是要使用的命令。现在问题变成了:我们如何提供正确的文件名集?

Shell 确实有一种命令替换形式:

command1 $(command2)

告诉 shell 运行command2,将其输出作为一系列以空格分隔的单词,将它们分解为单独的参数,然后command1像我们输入这些参数一样运行。所以:

git reset HEAD -- $(command2)

如果我们有一个列出我们想要的文件的命令,就可以了git reset HEAD。这--是为了确保即使command2打印一个类似的文件名--mixed,它也不会被视为git reset.

我们还需要找到command2;我们稍后再谈。

但是这里有一个明显的缺陷:如果我们有一个名为READ MEand的文件READ ME TOO,即在它们的实际名称中包含文字空格怎么办?shell 将在空白处拆分,并将它们视为文件READMETOO. 还有一个更微妙的缺陷,即参数列表的长度git reset通常受底层操作系统的限制。 使用xargs绕过其中的第二个:

xargs cmd with initial arguments < very-long-list-of-file-names

将运行cmd with initial arguments file1 file2 file3, then cmd with initial arguments file4 file5 file6,依此类推,只将适合的文件名聚集在一起。该xargs命令通过从其标准输入中一次读取每个名称来生成添加的参数。

但是,这仍然存在一些空白问题:如果输入文件列出了READ MEthen READ ME TOO,我们仍然会错误地git reset使用 names README、 and TOOREADME重复运行。为了解决这个问题,我们可以使用xargs -0,它将其输入视为由\0(ASCII NUL) 字节分隔的名称组成。这适用于输出带有\0分隔符的文件名的程序,例如find ... -print0. 事实证明,我们想要用来生成文件名的 Git 命令具有相同的选项。

无论如何,考虑到这一点,我们可以直接回到我们之前提到的事实,即git status运行两个git diff命令。第一个——它产生它为提交而调用的文件列表——HEAD与索引进行比较。我们可以手动运行git diff --name-status自己以获取相同的列表,但格式略有不同。我们将需要--cached标志,它指示与 Git 的索引git diff进行比较。1HEAD

在 的输出中git diff --name-status,我们会看到,例如:

A       file-new.ext
M       file-old.ext

前面的状态字母A来自一个相对较短的可能字母菜单:表示Added,即全新的文件,M表示Modified。这很好:状态信让我们只挑选添加的文件。但是我们仍然需要分割输入线的其余部分。这通常不太难——Git 打印字母,然后是制表符,然后文件名以换行符结尾——但如果我们想避免名称中的空格复杂性,我们有一些不错的选择:

  • --diff-filter=letters让我们告诉git diff打印那些以这些字母代码之一开头的行,因此意味着只打印dded文件;git diff --diff-filter=AA
  • --name-only告诉git diff隐藏状态码字母本身(它仍然在内部生成,只是没有打印出来);和
  • -z告诉git diff产生适合的机器可读输出xargs

把这些放在一起,我们得到:

git diff --cached --name-only -z --diff-filter=A

从与索引 ( )的比较结果中,以适合程序的零标志 ( ) 的形式仅打印Added 文件 ( --diff-filter),或者只是它们的名称 ( ) 。因此,我们只需要将其通过管道传输到. 2--name-onlyHEAD--cached-zxargsxargs -0


1如果你愿意,你可以使用--stagedflag,意思是一样的。我倾向于使用,--cached因为git rm仍然没有. 这些使用 Git 索引的第三个名称:较旧的 Git 文档有时将其称为缓存。一个命令,试图区分索引()和缓存(),但总的来说,我认为这有点棘手了:这里表示索引副本和工作树副本,而仅表示索引副本--staged--cachedgit applygit apply --indexgit apply --cached--index--cached

2 flag 是-zGit 命令的,但-0xargs本身是一个小烦恼。你只需要记住这一点,或者定期检查文档。


警告/注意事项

git diff命令非常好,但是如果您将其转换为脚本,它可能会有一个缺陷。这就是 Git 文档所称的瓷器命令。这意味着它遵循用户配置,即您可以设置的项目git config

此处特别重要的一项用户配置项是diff.renames. 如果diff.renames设置为true, agit diff将查找重命名的文件。

在 Git 中,重命名是事后检测的问题。也就是说,虽然有git mv命令,git mv但实际上并没有记录重命名。它只是删除一个索引名称并创建另一个不同的名称来保存文件的内容。这样,在 之后git mv old new,您的下一个提交缺少一个名为 的文件old,但一个名为new. 当前提交中的文件内容将与新提交中的文件内容相匹配,当然前提是您也不要更改索引副本的内容。oldnew

因此,给定任何两个提交,git diff只需列出所有左侧提交的文件名和所有右侧提交的文件名:

  • 如果两边都有一个文件this-name-did-not-change假定这是同一个文件,并且只是查看它以查看文件中的更改(如果有的话

  • 如果左侧有一个名为 的文件removeme,而右侧有所有相同的文件,removeme只是没有了,差异会说这个文件已被删除。

  • 如果左侧有 100 个文件,右侧有 101 个文件,其中 100 个具有相同的名称,第 101 个具有 name new-file,则差异会说添加了新文件。

  • 但是——这里是重命名检测器——假设左侧和右侧都有 100 个文件,而其中 99 个文件具有相同的名称。同时在左侧,名为的文件old没有右侧的文件名为old,而在右侧,有一个名为 的新文件new。Git 现在将把旧文件的内容old与新文件new.

如果这两个文件足够相似3 Git 现在将声称从旧提交到新提交的差异之一——也许是唯一的不同——是文件old重命名为文件new。此状态字母为R,并将git diff使用此状态字母打印两个文件名。(这意味着git diff -z如果您允许状态,则必须非常仔细地对输出进行机器解释R!)

除了意识到git diff总是将重命名作为事后测试——并且文件只有在“足够相似”时才被认为是重命名(再次参见脚注 3)——这里的关键考虑因素是,它甚至git diff只会寻找如果重命名检测器打开,则重命名。如果它打开,您可以获得R带有两个文件名的字母。如果它关闭,您将获得一个D用于从左到右“删除”的文件和一个A用于从左到右“添加”的文件,而不是R左侧名称作为旧名称的文件,并且右侧名称作为新名称。

那么,重命名检测器什么时候开启呢?好吧,该--find-renames选项将其打开,但我们没有在git diff命令中使用它。但是git diff是一个瓷器命令,所以它服从用户配置。如果用户设置diff.renamestrue,重命名检测器也会打开,除非我们添加--no-find-renames. 如果用户设置diff.renamesfalse,重命名检测器将关闭,除非我们使用--find-renames. 因此,如果我们想要非常具体,我们可以添加--find-renamesor --no-find-renames

大多数时候这无关紧要,但是如果我们同时有新创建的文件一些已删除的文件,或者如果我们已经运行git mv或以其他方式修改了 Git 索引中的名称,4 git diff --cached可能会决定某个文件已重命名,而不是查找一个文件被删除,另一个文件是新添加的。

解决这种每个用户的行为——如果你确实解决它;也许你更喜欢它——你可以:

  • 添加显式标志,--find-renames--no-find-renames;或者
  • 用于强制设置;或者git -c diff.renames=setting
  • 使用管道命令 与索引git diff-index --cached HEAD进行比较。HEAD

最后一个选项 usinggit diff-index --cached HEAD与 running 类似git diff --cached,只是它不读取用户的配置,并且重命名检测器始终处于关闭状态,除非您添加--find-renames到参数中。

请注意,在没有diff.renames 设置的情况下,重命名检测器在 Git 2.9 之前的 Git 版本中默认为关闭,但在 Git 2.9 及更高版本中默认为打开(用于瓷器命令)。


3相似度如何才算足够? 如何衡量相似性? 这是一个单独问题的主题,但值得注意的是,如果旧文件和新文件是 100% 逐字节匹配,重命名检测器将始终声明要重命名的文件,并且工作速度非常快。如果甚至有一个字节的差异,它就会工作得更慢。

4git mv命令只是调整事物的好方法;有一个管道命令,git update-index可以做任何事情。


推荐阅读