git - 将所有新文件从暂存区移动到单行中的工作区
问题描述
是否有一个优雅的命令将 HEAD 所有新文件仅从暂存区(待提交列表)重置回工作区?这发生在我做了git add -- '*.py'
. 一个相关的问题是如何在上述语法中只添加对现有文件的更改。
解决方案
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
在索引中命名的文件被认为是A
dded(“新”),则它不能存在于HEAD
. 这反过来意味着:
git reset HEAD -- file-new.ext
只会从 Git 的索引中删除副本。file.ext
同时,如果一个名为的文件file-old.ext
被认为被M
修改,它必须同时存在于HEAD
Git 的索引中。这反过来意味着:
git reset HEAD -- file-old.ext
将提交的版本复制file-old.ext
回 Git 的索引。
考虑到这些,让我们首先重新审视标题中问题的措辞:
将所有新文件从暂存区移动到单行中的工作区
有问题的两个示例文件 (file-new.ext
和file-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 ME
and的文件READ ME TOO
,即在它们的实际名称中包含文字空格怎么办?shell 将在空白处拆分,并将它们视为文件READ
、ME
和TOO
. 还有一个更微妙的缺陷,即参数列表的长度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 ME
then READ ME TOO
,我们仍然会错误地git reset
使用 names READ
、ME
、 and TOO
、READ
和ME
重复运行。为了解决这个问题,我们可以使用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
来自一个相对较短的可能字母菜单:表示A
dded,即全新的文件,M
表示M
odified。这很好:状态信让我们只挑选添加的文件。但是我们仍然需要分割输入线的其余部分。这通常不太难——Git 打印字母,然后是制表符,然后文件名以换行符结尾——但如果我们想避免名称中的空格复杂性,我们有一些不错的选择:
--diff-filter=letters
让我们告诉只git diff
打印那些以这些字母代码之一开头的行,因此意味着只打印dded文件;git diff --diff-filter=A
A
--name-only
告诉git diff
隐藏状态码字母本身(它仍然在内部生成,只是没有打印出来);和-z
告诉git diff
产生适合的机器可读输出xargs
。
把这些放在一起,我们得到:
git diff --cached --name-only -z --diff-filter=A
从与索引 ( )的比较结果中,以适合程序的零标志 ( ) 的形式仅打印A
dded 文件 ( --diff-filter
),或者只是它们的名称 ( ) 。因此,我们只需要将其通过管道传输到. 2--name-only
HEAD
--cached
-z
xargs
xargs -0
1如果你愿意,你可以使用--staged
flag,意思是一样的。我倾向于使用,--cached
因为git rm
仍然没有. 这些使用 Git 索引的第三个名称:较旧的 Git 文档有时将其称为缓存。一个命令,试图区分索引()和缓存(),但总的来说,我认为这有点太棘手了:这里表示索引副本和工作树副本,而仅表示索引副本。--staged
--cached
git apply
git apply --index
git apply --cached
--index
--cached
2 flag 是-z
Git 命令的,但-0
它xargs
本身是一个小烦恼。你只需要记住这一点,或者定期检查文档。
警告/注意事项
该git diff
命令非常好,但是如果您将其转换为脚本,它可能会有一个缺陷。这就是 Git 文档所称的瓷器命令。这意味着它遵循用户配置,即您可以设置的项目git config
。
此处特别重要的一项用户配置项是diff.renames
. 如果diff.renames
设置为true
, agit diff
将查找重命名的文件。
在 Git 中,重命名是事后检测的问题。也就是说,虽然有git mv
命令,git mv
但实际上并没有记录重命名。它只是删除一个索引名称并创建另一个不同的名称来保存文件的内容。这样,在 之后git mv old new
,您的下一个提交缺少一个名为 的文件old
,但有一个名为new
. 当前提交中的文件内容将与新提交中的文件内容相匹配,当然前提是您也不要更改索引副本的内容。old
new
因此,给定任何两个提交,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.renames
为true
,重命名检测器也会打开,除非我们添加--no-find-renames
. 如果用户设置diff.renames
为false
,重命名检测器将关闭,除非我们使用--find-renames
. 因此,如果我们想要非常具体,我们可以添加--find-renames
or --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
可以做任何事情。
推荐阅读
- sql - dbt 宏在 sql 调用中迭代列表中的项目?
- python - 有没有办法在满足条件后终止我的程序?(如果玩家赢了游戏)
- typescript - 打字稿中非平凡数据结构的最佳实践
- html - 为什么 y 轴在 HTML DOM 和 SVG 中是倒置的?
- android - 如何在项目类型“Android App (Xamarin)”中使用 RefreshView
- r - 如何使用 r 中的虚拟变量创建分类变量?
- reactjs - 使用 React 找不到搜索时,我的代码显示错误
- ios - 是否需要请求 iOS 按需资源的预取标签?
- android - 如何在 Android 10 上使用 html 文件中的超链接引用本地文件?
- shell - 如何对目录中的所有文件使用 Sox