如何在 git 中进行重命名而不进行后续编辑?
How to stage a rename without subsequent edits in git?
我有一个文件,我已经重命名并编辑了它。我想告诉 Git 进行重命名,而不是内容修改。也就是说,我想分阶段删除旧文件名,并用新文件名添加旧文件内容。
所以我有这个:
Changes not staged for commit:
deleted: old-name.txt
Untracked files:
new-name.txt
但想要这个:
Changes to be committed:
new file: new-name.txt
deleted: old-name.txt
Changes not staged for commit:
modified: new-name.txt
或者这个:
Changes to be committed:
renamed: old-name.txt -> new-name.txt
Changes not staged for commit:
modified: new-name.txt
(相似性度量应为 100%)。
我想不出一个简单的方法来做到这一点。
是否有获取特定文件的特定修订版内容并将其添加到指定路径下的 git 暂存区的语法?
删除部分,git rm
,可以:
$ git rm old-name.txt
这是我正在努力解决的重命名的添加部分。 (我可以保存新内容,签出新副本(旧内容),shell、git add
中的 mv
,然后恢复新内容,但这似乎很长的路要走!)
谢谢!
Git 并没有真正重命名。它们全部以"after the fact"方式计算:git将一个提交与另一个提交进行比较,在比较时间,决定是否有重命名。这意味着 git 是否考虑某事 "a rename" 动态变化。我知道你问的是你还没有做出的承诺,但请耐心等待,这真的很相关(但答案会很长)。
当您询问 git(通过 git show
或 git log -p
或 git diff HEAD^ HEAD
)"what happened in the last commit" 时,它会运行上一次提交的差异(HEAD^
或 HEAD~1
或前一次提交的实际原始 SHA-1——其中任何一个都可以识别它)和当前提交 (HEAD
)。在进行比较时,它可能会发现曾经有一个 old.txt
而现在已经不存在了;以前没有 new.txt
但现在有了
这些文件名——过去存在但不存在的文件,以及现在存在但不存在的文件——被放入标记为 "candidates for rename" 的堆中。然后,对于堆中的每个名字,git 比较 "old contents" 和 "new contents"。 完全匹配 的比较非常简单,因为 git 将内容缩减为 SHA-1;如果完全匹配失败,git 切换到可选的 "are the contents at least similar" diff 来检查重命名。对于 git diff
,此可选步骤由 -M
标志控制。对于其他命令,它要么由您的 git config
值设置,要么硬编码到命令中。
现在,回到暂存区和git status
:git 存储在索引/暂存区的基本上是"a prototype for the next commit"。当您 git add
某些内容时,git 会在该点存储文件内容,在此过程中计算 SHA-1,然后将 SHA-1 存储在索引中。当你 git rm
某些东西时,git 会在索引中存储一个注释 "this path name is being deliberately removed on the next commit"。
git status
命令,然后,简单地做一个差异——或者实际上,两个差异:HEAD
与索引,用于将要提交的内容;和索引与工作树,因为 可能 将(但尚未)提交。
在第一个差异中,git 使用与往常相同的机制来检测重命名。如果 HEAD
提交中有一条路径在索引中消失了,并且索引中有一条路径是新的而不是 HEAD
提交中的路径,则它是重命名检测的候选者。 git status
命令将重命名检测硬连接到 "on"(并且文件计数限制为 200;只有一个候选重命名检测这个限制就足够了)。
这对您的情况意味着什么?好吧,你重命名了一个文件(没有使用 git mv
,但这并不重要,因为 git status
在 git status
时找到了重命名,或者找不到它),现在有了新文件的更新版本。
如果你 git add
新版本,那个较新的版本进入回购协议,它的 SHA-1 在索引中,当 git status
做一个差异时,它会比较新的和老的。如果它们至少“50% 相似”(git status
的固定值),git 会告诉您文件已重命名。
当然,git add
-ing 修改后的 内容并不完全符合您的要求:您想在文件为 [=109 的地方进行中间提交=]仅重命名,即使用新名称但旧内容的树的提交。
您不必执行此操作,因为所有上述动态重命名检测。如果您想要 这样做(无论出于何种原因)......好吧,git 并没有那么容易。
最直接的方法就是按照你的建议:将修改的内容移到别处,使用git checkout -- old-name.txt
,然后git mv old-name.txt new-name.txt
,然后提交。 git mv
将重命名 index/staging-area 中的文件,并重命名工作树版本。
如果 git mv
有一个像 git rm
那样的 --cached
选项,你可以只 git mv --cached old-name.txt new-name.txt
然后 git commit
。第一步将重命名索引中的文件,而不触及工作树。但它没有:它坚持要覆盖工作树版本,并且坚持旧名称必须存在于工作树中才能启动。
在不触及工作树的情况下执行此操作的单步方法是使用 git update-index --index-info
,但这也有些混乱(我稍后会展示它)。幸运的是,我们还有最后一件事可以做。我设置了与您相同的情况,方法是将旧名称重命名为新名称并修改文件:
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: old-name.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
new-name.txt
我们现在要做的是,先手动把文件改回原来的名字,然后用git mv
再次切换到新名字:
$ mv new-name.txt old-name.txt
$ git mv old-name.txt new-name.txt
这次 git mv
更新索引中的名称,但 保留原始内容 作为索引 SHA-1,但 移动工作-树版本(新内容)在工作树中的位置:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: old-name.txt -> new-name.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: new-name.txt
现在 git commit
提交重命名,但不包含新内容。
(注意这取决于没有旧名称的新文件!)
使用 git update-index
怎么样?好吧,首先让我们把事情恢复到 "changed in work-tree, index matches HEAD commit" 状态:
$ git reset --mixed HEAD # set index=HEAD, leave work-tree alone
现在让我们看看 old-name.txt
的索引中有什么:
$ git ls-files --stage -- old-name.txt
100644 2b27f2df63a3419da26984b5f7bafa29bdf5b3e3 0 old-name.txt
所以,我们需要 git update-index --index-info
做的是清除 old-name.txt
的条目,但为 new-name.txt
创建一个其他相同的条目:
$ (git ls-files --stage -- old-name.txt;
git ls-files --stage -- old-name.txt) |
sed -e \
'1s/^[0-9]* [0-9a-f]*/000000 0000000000000000000000000000000000000000/' \
-e '2s/old-name.txt$/new-name.txt/' |
git update-index --index-info
(注意:为了发帖,我把上面的内容打散了,我输入的时候都是一行;在 sh/bash 中,考虑到我添加的反斜杠,它应该像这样打散继续 "sed" 命令。
还有其他一些方法可以做到这一点,但是简单地提取索引条目两次并将第一个修改为删除,第二个修改为新名称似乎是最简单的,因此使用了 sed
命令。第一个替换更改文件模式(100644,但任何模式都会变成全零)和 SHA-1(匹配任何 SHA-1,替换为 git 的特殊全零 SHA-1),以及第二个在替换名称时单独保留模式和 SHA-1。
当更新索引完成时,索引记录了旧路径的删除和新路径的添加(与旧路径中的模式和 SHA-1 相同)。
请注意,如果索引具有 old-name.txt
的未合并条目,这可能会严重失败,因为文件可能还有其他阶段(1 到 3)。
@torek 给出了非常清晰和完整的答案。那里有很多非常有用的细节;非常值得一读。
但是,为了那些匆忙的人,给出的非常简单的解决方案的关键是:
What we do now is, first, manually put the file back under its old
name, then use git mv to switch again to the new name:
$ mv new-name.txt old-name.txt
$ git mv old-name.txt new-name.txt
(我只是缺少 mv
后背,使 git mv
成为可能。)
如果您觉得有帮助,请为 @torek 的 回答点赞。
我使用 Git GUI Sourcetree。它具有暂存和取消暂存文件“块”或突出显示各个行并暂存或取消暂存它们的功能。
我能够暂存已删除的文件和新文件,让 GUI 将其检测为“重命名”。然后我检查并突出显示所有已更改的行并取消暂存它们。所以现在我已经重命名了,但是文件的 none 发生了变化。
我知道不是每个人都使用这个 UI。但据我所知,它是基于此处描述的“补丁”的交互式分期: https://git-scm.com/book/en/v2/Git-Tools-Interactive-Staging 。所以我会调查一下。然后 un-stage 补丁。
我有一个文件,我已经重命名并编辑了它。我想告诉 Git 进行重命名,而不是内容修改。也就是说,我想分阶段删除旧文件名,并用新文件名添加旧文件内容。
所以我有这个:
Changes not staged for commit:
deleted: old-name.txt
Untracked files:
new-name.txt
但想要这个:
Changes to be committed:
new file: new-name.txt
deleted: old-name.txt
Changes not staged for commit:
modified: new-name.txt
或者这个:
Changes to be committed:
renamed: old-name.txt -> new-name.txt
Changes not staged for commit:
modified: new-name.txt
(相似性度量应为 100%)。
我想不出一个简单的方法来做到这一点。
是否有获取特定文件的特定修订版内容并将其添加到指定路径下的 git 暂存区的语法?
删除部分,git rm
,可以:
$ git rm old-name.txt
这是我正在努力解决的重命名的添加部分。 (我可以保存新内容,签出新副本(旧内容),shell、git add
中的 mv
,然后恢复新内容,但这似乎很长的路要走!)
谢谢!
Git 并没有真正重命名。它们全部以"after the fact"方式计算:git将一个提交与另一个提交进行比较,在比较时间,决定是否有重命名。这意味着 git 是否考虑某事 "a rename" 动态变化。我知道你问的是你还没有做出的承诺,但请耐心等待,这真的很相关(但答案会很长)。
当您询问 git(通过 git show
或 git log -p
或 git diff HEAD^ HEAD
)"what happened in the last commit" 时,它会运行上一次提交的差异(HEAD^
或 HEAD~1
或前一次提交的实际原始 SHA-1——其中任何一个都可以识别它)和当前提交 (HEAD
)。在进行比较时,它可能会发现曾经有一个 old.txt
而现在已经不存在了;以前没有 new.txt
但现在有了
这些文件名——过去存在但不存在的文件,以及现在存在但不存在的文件——被放入标记为 "candidates for rename" 的堆中。然后,对于堆中的每个名字,git 比较 "old contents" 和 "new contents"。 完全匹配 的比较非常简单,因为 git 将内容缩减为 SHA-1;如果完全匹配失败,git 切换到可选的 "are the contents at least similar" diff 来检查重命名。对于 git diff
,此可选步骤由 -M
标志控制。对于其他命令,它要么由您的 git config
值设置,要么硬编码到命令中。
现在,回到暂存区和git status
:git 存储在索引/暂存区的基本上是"a prototype for the next commit"。当您 git add
某些内容时,git 会在该点存储文件内容,在此过程中计算 SHA-1,然后将 SHA-1 存储在索引中。当你 git rm
某些东西时,git 会在索引中存储一个注释 "this path name is being deliberately removed on the next commit"。
git status
命令,然后,简单地做一个差异——或者实际上,两个差异:HEAD
与索引,用于将要提交的内容;和索引与工作树,因为 可能 将(但尚未)提交。
在第一个差异中,git 使用与往常相同的机制来检测重命名。如果 HEAD
提交中有一条路径在索引中消失了,并且索引中有一条路径是新的而不是 HEAD
提交中的路径,则它是重命名检测的候选者。 git status
命令将重命名检测硬连接到 "on"(并且文件计数限制为 200;只有一个候选重命名检测这个限制就足够了)。
这对您的情况意味着什么?好吧,你重命名了一个文件(没有使用 git mv
,但这并不重要,因为 git status
在 git status
时找到了重命名,或者找不到它),现在有了新文件的更新版本。
如果你 git add
新版本,那个较新的版本进入回购协议,它的 SHA-1 在索引中,当 git status
做一个差异时,它会比较新的和老的。如果它们至少“50% 相似”(git status
的固定值),git 会告诉您文件已重命名。
当然,git add
-ing 修改后的 内容并不完全符合您的要求:您想在文件为 [=109 的地方进行中间提交=]仅重命名,即使用新名称但旧内容的树的提交。
您不必执行此操作,因为所有上述动态重命名检测。如果您想要 这样做(无论出于何种原因)......好吧,git 并没有那么容易。
最直接的方法就是按照你的建议:将修改的内容移到别处,使用git checkout -- old-name.txt
,然后git mv old-name.txt new-name.txt
,然后提交。 git mv
将重命名 index/staging-area 中的文件,并重命名工作树版本。
如果 git mv
有一个像 git rm
那样的 --cached
选项,你可以只 git mv --cached old-name.txt new-name.txt
然后 git commit
。第一步将重命名索引中的文件,而不触及工作树。但它没有:它坚持要覆盖工作树版本,并且坚持旧名称必须存在于工作树中才能启动。
在不触及工作树的情况下执行此操作的单步方法是使用 git update-index --index-info
,但这也有些混乱(我稍后会展示它)。幸运的是,我们还有最后一件事可以做。我设置了与您相同的情况,方法是将旧名称重命名为新名称并修改文件:
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: old-name.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
new-name.txt
我们现在要做的是,先手动把文件改回原来的名字,然后用git mv
再次切换到新名字:
$ mv new-name.txt old-name.txt
$ git mv old-name.txt new-name.txt
这次 git mv
更新索引中的名称,但 保留原始内容 作为索引 SHA-1,但 移动工作-树版本(新内容)在工作树中的位置:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: old-name.txt -> new-name.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: new-name.txt
现在 git commit
提交重命名,但不包含新内容。
(注意这取决于没有旧名称的新文件!)
使用 git update-index
怎么样?好吧,首先让我们把事情恢复到 "changed in work-tree, index matches HEAD commit" 状态:
$ git reset --mixed HEAD # set index=HEAD, leave work-tree alone
现在让我们看看 old-name.txt
的索引中有什么:
$ git ls-files --stage -- old-name.txt
100644 2b27f2df63a3419da26984b5f7bafa29bdf5b3e3 0 old-name.txt
所以,我们需要 git update-index --index-info
做的是清除 old-name.txt
的条目,但为 new-name.txt
创建一个其他相同的条目:
$ (git ls-files --stage -- old-name.txt;
git ls-files --stage -- old-name.txt) |
sed -e \
'1s/^[0-9]* [0-9a-f]*/000000 0000000000000000000000000000000000000000/' \
-e '2s/old-name.txt$/new-name.txt/' |
git update-index --index-info
(注意:为了发帖,我把上面的内容打散了,我输入的时候都是一行;在 sh/bash 中,考虑到我添加的反斜杠,它应该像这样打散继续 "sed" 命令。
还有其他一些方法可以做到这一点,但是简单地提取索引条目两次并将第一个修改为删除,第二个修改为新名称似乎是最简单的,因此使用了 sed
命令。第一个替换更改文件模式(100644,但任何模式都会变成全零)和 SHA-1(匹配任何 SHA-1,替换为 git 的特殊全零 SHA-1),以及第二个在替换名称时单独保留模式和 SHA-1。
当更新索引完成时,索引记录了旧路径的删除和新路径的添加(与旧路径中的模式和 SHA-1 相同)。
请注意,如果索引具有 old-name.txt
的未合并条目,这可能会严重失败,因为文件可能还有其他阶段(1 到 3)。
@torek 给出了非常清晰和完整的答案。那里有很多非常有用的细节;非常值得一读。
但是,为了那些匆忙的人,给出的非常简单的解决方案的关键是:
What we do now is, first, manually put the file back under its old name, then use git mv to switch again to the new name:
$ mv new-name.txt old-name.txt $ git mv old-name.txt new-name.txt
(我只是缺少 mv
后背,使 git mv
成为可能。)
如果您觉得有帮助,请为 @torek 的 回答点赞。
我使用 Git GUI Sourcetree。它具有暂存和取消暂存文件“块”或突出显示各个行并暂存或取消暂存它们的功能。
我能够暂存已删除的文件和新文件,让 GUI 将其检测为“重命名”。然后我检查并突出显示所有已更改的行并取消暂存它们。所以现在我已经重命名了,但是文件的 none 发生了变化。
我知道不是每个人都使用这个 UI。但据我所知,它是基于此处描述的“补丁”的交互式分期: https://git-scm.com/book/en/v2/Git-Tools-Interactive-Staging 。所以我会调查一下。然后 un-stage 补丁。