如何在没有后续编辑的情况下进行重命名? [英] How to stage a rename without subsequent edits in git?
问题描述
我有一个我已经重新命名并编辑的文件。我想告诉Git进行重命名,但不是内容修改。也就是说,我希望删除旧的文件名,并添加旧的文件内容与新的文件名。
所以我有这个:
未进行提交的更改:
已删除:old-name.txt
未记录的文件:
new-name.txt
这:
要提交的更改:
新文件:new-name.txt
已删除:old-name.txt
不进行提交的更改:
修改:new-name.txt
或者:
要提交的更改:
已更名为:old-name.txt - > new-name.txt
不进行提交的更改:
修改:new-name.txt
(其中相似性度量应该是100%)。
我想不出一种简单的方法来做这是。
是否有获取特定文件的特定修订内容的语法,并将其添加到指定路径下的git临时区域?
删除部分(
$ git rm old-name.txt
这是添加部分我正在努力争取的重新命名。 (我可以保存新内容,在shell中检出新副本(用于旧内容), mv
, git add $ c $ )
谢谢!
Git并没有真的重命名。它们都是以事后的方式计算出来的:git比较一个提交与另一个提交,而比较时间决定是否有重命名。这意味着git是否认为重命名是动态变化的。我知道你问的是一个你还没有做出的承诺,但是忍耐着我,这真的都是配合的(但答案会很长)。
< hr>
当你问git(通过 git show
或 git log -p
或 git diff HEAD ^ HEAD
)上次提交时发生了什么,它运行前一个提交( HEAD ^
或 HEAD〜1
或前一个提交的实际原始SHA-1,其中任何一个都可以识别它)和当前提交( HEAD
)。在制作这个差异时,它可能会发现过去有一个 old.txt
,而不再是这样;并且没有 new.txt
但现在有了。
这些文件名称 - 曾经存在的文件但是没有,并且现在没有的文件被放入标有重命名候选人的桩中。然后,对于堆中的每个名称,git比较旧内容和新内容。由于git将内容减少到SHA-1的方式,精确匹配的比较是非常容易的;如果完全匹配失败,git会切换到一个可选的至少是类似的内容diff来检查重命名。使用 git diff
这个可选步骤由 -M
标志控制。使用其他命令,它可以由 git config
值设置,或者硬编码到命令中。
现在回到暂存区和 git status
:索引/暂存区中存储的内容基本上是下一次提交的原型。当你 git add
>时,git将文件内容存储在该点,在这个过程中计算SHA-1,然后将SHA-1存储在索引中。当你 git rm
时,git会在索引中存储一条记录,说明在下一次提交时有意删除此路径名称。
git status
命令然后简单地做一个差异 - 或者真的是两个差异: HEAD
对索引,为什么要承诺;以及索引与工作树之间的关系,以确定可以(但尚未)被提交。
git使用与往常一样的机制来检测重命名。如果 HEAD
提交中有一个路径在索引中消失了,并且索引中的路径是新的,而不是 HEAD
commit,它是重命名检测的候选对象。 git status
命令将硬件重新命名检测设置为开启(并且文件计数限制为200;只有一个重新命名检测候选人,此限制很多)。
这对你的情况意味着什么?那么,你重命名一个文件(不使用 git mv
,但它并不重要,因为 git status
找到重命名,或无法找到它,在 git status
时间),现在有一个更新的,不同版本的新文件。
如果您的 git add
新版本,该新版本进入回购库,并且其SHA-1位于索引中,当 git status
做一个差异,它会比较新的和旧的。如果它们至少50%相似( git status
的硬连线值),git会告诉你文件被重命名。
当然, git add
- 修改过的内容并不完全符合您的要求:您希望执行仅在 中重新命名的中间提交,即使用新名称的树进行提交,但使用旧内容。
因为所有上述动态重命名检测都有这样做。如果您希望这样做(无论出于何种原因)......好吧,git并不是那么容易。
最直接的方法就像你所建议的那样:把修改后的内容移到某处,使用 git checkout - old-name.txt
,然后 git mv old-name.txt new-name.txt
,然后提交。 git mv
将重命名索引/登台区域中的文件,并重命名工作树版本。
<如果
git mv
有一个 - 缓存的
选项,比如 git rm
可以,只需 git mv --cached old-name.txt new-name.txt
然后 git commit
。第一步将重命名索引中的文件,而不触及工作树。但它并没有:它坚持重写工作树版本,并坚持要求旧的名字必须存在于工作树中才能开始。 单曲在不触及工作树的情况下执行此操作的一步方法是使用 git update-index --index-info
,但这也有点麻烦(我会显示无论如何它在一瞬间)。幸运的是,我们还有最后一件事可以做。我已经设置了相同的情况,将旧名称重命名为新名称并修改文件:
$ git status
在分支主机上
更改没有为commit提交:
(使用git add / rm< file> ...更新将提交的内容)
(使用git checkout - < file>放弃工作目录中的更改)
deleted:old-name.txt
未记录的文件:
(使用git add< file> ...来包含将要提交的内容)
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
$ p $现在只需
(新内容)到工作树中:在分支大师
要提交的更改:
(使用git reset HEAD< file> ...停用)
已更名:old-name.txt - > ; new-name.txt
没有为commit提交更改:
(使用git add< file> ...更新将提交的内容)
(使用 git checkout - < file> ...放弃工作目录中的更改)
修改:new-name.txt
git commit
就可以进行重命名,而不是新内容。(请注意,这取决于没有旧名称的新文件!)
如何使用
git update-index
?首先让我们回到改变工作树,索引匹配HEAD提交状态:$ git reset --mixed HEAD#set index = HEAD,离开工作树
现在让我们来看看index
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)。
请注意,如果索引没有为
old- name.txt
,因为文件可能有其他阶段(1到3)。I have a file that I've renamed and then edited. I would like to tell Git to stage the rename, but not the content modifications. That is, I wish to stage the deletion of the old file name, and the addition of the old file contents with the new file name.
So I have this:
Changes not staged for commit: deleted: old-name.txt Untracked files: new-name.txt
but want either this:
Changes to be committed: new file: new-name.txt deleted: old-name.txt Changes not staged for commit: modified: new-name.txt
or this:
Changes to be committed: renamed: old-name.txt -> new-name.txt Changes not staged for commit: modified: new-name.txt
(where the similarity measure should be 100%).
I can't think of a straightforward way to do this.
Is there syntax for getting the contents of a specific revision of a specific file, and adding this to the git staging area under a specified path?
The delete part, with
git rm
, is fine:$ git rm old-name.txt
It's the add part of the rename I'm struggling with. (I could save the new contents, checkout a fresh copy (for the old contents),
mv
in the shell,git add
, and then recover the new contents, but that seems like a very long way around!)Thanks!
解决方案Git doesn't really do renames. They're all computed in an "after the fact" fashion: git compares one commit with another and, at compare time, decides if there was a rename. This means that whether git considers something "a rename" changes dynamically. I know you're asking about a commit you haven't even made yet, but bear with me, this really all does tie in (but the answer will be long).
When you ask git (via
git show
orgit log -p
orgit diff HEAD^ HEAD
) "what happened in the last commit", it runs a diff of the previous commit (HEAD^
orHEAD~1
or the actual raw SHA-1 for the previous commit—any of these will do to identify it) and the current commit (HEAD
). In making that diff it may discover that there used to be anold.txt
and isn't any more; and there was nonew.txt
but there is now.These file names—files that used to be there but aren't, and files that are there now that weren't—are put into pile marked "candidates for rename". Then, for each name in the pile, git compares "old contents" and "new contents". The comparison for exact match is super-easy because of the way git reduces contents to SHA-1s; if the exact-match fails, git switches to an optional "are the contents at least similar" diff to check for renames. With
git diff
this optional step is controlled by the-M
flag. With other commands it's either set by yourgit config
values, or hardcoded into the command.Now, back to the staging area and
git status
: what git stores in the index / staging-area is basically "a prototype for the next commit". When yougit add
something, git stores the file contents right at that point, computing the SHA-1 in the process and then storing the SHA-1 in the index. When yougit rm
something, git stores a note in the index saying "this path name is being deliberately removed on the next commit".The
git status
command, then, simply does a diff—or really, two diffs:HEAD
vs index, for what is going to be committed; and index vs work-tree, for what could be (but isn't yet) going to be committed.In that first diff, git uses the same mechanism as always to detect renames. If there's a path in the
HEAD
commit that is gone in the index, and a path in the index that's new and not in theHEAD
commit, it's a candidate for rename-detection. Thegit status
command hardwires rename detection to "on" (and the file count limit to 200; with just one candidate for rename detection this limit is plenty).
What does all this mean for your case? Well, you renamed a file (without using
git mv
, but it doesn't really matter becausegit status
finds the rename, or fails to find it, atgit status
time), and now have a newer, different version of the new file.If you
git add
the new version, that newer version goes into the repo, and its SHA-1 is in the index, and whengit status
does a diff it will compare the new and old. If they're at least "50% similar" (the hardwired value forgit status
), git will tell you the file is renamed.Of course,
git add
-ing the modified contents is not quite what you asked for: you wanted to do an intermediate commit where the file is only renamed, i.e., a commit with a tree with the new name, but the old contents.You don't have to do this, because of all of the above dynamic rename detection. If you want to do it (for whatever reason) ... well, git doesn't make it all that easy.
The most straightforward way is just as you suggest: move the modified contents somewhere out of the way, use
git checkout -- old-name.txt
, thengit mv old-name.txt new-name.txt
, then commit. Thegit mv
will both rename the file in the index/staging-area, and rename the work-tree version.If
git mv
had a--cached
option likegit rm
does, you could justgit mv --cached old-name.txt new-name.txt
and thengit commit
. The first step would rename the file in the index, without touching the work-tree. But it doesn't: it insists on overwriting the work-tree version, and it insists that the old name must exist in the work-tree to start.The single step method for doing this without touching the work-tree is to use
git update-index --index-info
, but that, too, is somewhat messy (I'll show it in a moment anyway). Fortunately, there's one last thing we can do. I've set up the same situation you had, by renaming the old name to the new one and modifying the file:$ 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
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
This time
git mv
updates the name in the index, but keeps the original contents as the index SHA-1, yet moves the work-tree version (new contents) into place in the work-tree:$ 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
Now just
git commit
to make a commit with the rename in place, but not the new contents.(Note that this depends on there not being a new file with the old name!)
What about using
git update-index
? Well, first let's get things back to the "changed in work-tree, index matches HEAD commit" state:$ git reset --mixed HEAD # set index=HEAD, leave work-tree alone
Now let's see what's in the index for
old-name.txt
:$ git ls-files --stage -- old-name.txt 100644 2b27f2df63a3419da26984b5f7bafa29bdf5b3e3 0 old-name.txt
So, what we need
git update-index --index-info
to do is to wipe out the entry forold-name.txt
but make an otherwise identical entry fornew-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
(note: I broke the above up for posting purposes, it was all one line when I typed it in; in sh/bash, it should work broken-up like this, given the backslashes I added to continue the "sed" command).
There are some other ways to do this, but simply extracting the index entry twice and modifying the first one into a removal and the second with the new name seemed the easiest here, hence the
sed
command. The first substitution changes the file mode (100644 but any mode would be turned into all-zeros) and SHA-1 (match any SHA-1, replace with git's special all-zeros SHA-1), and the second leaves the mode and SHA-1 alone while replacing the name.When the update-index finishes, the index has recorded the removal of the old path and the addition of the new path (with same mode and SHA-1 as were in the old path).
Note that this could fail badly if the index had unmerged entries for
old-name.txt
since there might be other stages (1 to 3) for the file.这篇关于如何在没有后续编辑的情况下进行重命名?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!