如何让 Git 把删除的和新建的文件标记为文件移动?

762

我手动移动了一个文件,然后对它进行了修改。根据 Git 的记录,这是一个新文件和一个被删除的文件。有没有办法强制 Git 把它视为文件移动操作?


3
对于给定的文件 old_file.txt,执行 git mv old_file.txt new_file.txt 等同于执行以下三个步骤:git rm --cached old_file.txt、将 old_file.txt 重命名为 new_file.txtgit add new_file.txt。注意翻译时需保证内容原意不变且易于理解。 - Jarl
6
Jarl: 不是这样的。如果文件内部也有变更,git mv 命令不会将它们添加到缓存中,但 git add 会。我更喜欢将文件移回原处,这样就可以使用 git mv 命令,然后使用 git add -p 来查看我的修改集。 - dhardy
请查看我的脚本 https://github.com/Deathangel908/python-tricks/blob/master/git_delete_add_to_rename.py - deathangel908
可能是Is it possible to move/rename files in git and maintain their history?的重复问题。 - Michael Freidgeim
2
Git在过去的8年中得到了改进,如果只是一个文件,那么顶部答案https://dev59.com/FnRC5IYBdhLWcg3wCMg6#433142什么也没做...但你可以遵循https://dev59.com/FnRC5IYBdhLWcg3wCMg6#1541072将rm/add更新为mv/modify状态。 - dlamblin
显示剩余2条评论
15个回答

608

如果您的修改不是太严重,Git将自动检测到移动/重命名。只需git add新文件,然后git rm旧文件。然后,git status将显示是否已检测到重命名。

此外,对于目录中的移动,您可能需要:

  1. cd到该目录结构的顶部。
  2. 运行git add -A .
  3. 运行git status以验证“new file”现在是“renamed”文件

如果git status仍然显示“new file”而不是“renamed”,则需要遵循Hank Gay的建议分两个提交移动和修改。


133
澄清:'not too severe' 意味着根据 Git 使用的某些相似性指数,新文件和旧文件之间的相似度超过50%。 - pjz
244
有一件事让我卡了几分钟:如果重命名的文件和删除的文件没有被暂存以进行提交,那么它们将显示为一个删除和一个新文件。一旦将它们添加到暂存区索引中,它就会被识别为重命名。 - marczych
31
值得一提的是,当谈到“_Git将自动检测移动/重命名_”时,它会在您使用git statusgit loggit diff时进行检测,而不是在您使用git addgit mvgit rm时进行检测。进一步讲解重命名的检测,这只对已暂存的文件有意义。因此,对文件进行git mv操作并对文件进行更改,可能会导致在git status中将其视为重命名,但当您在文件上使用git stage(与git add相同)时,就会清楚地表明该更改过大,无法被检测为重命名。 - Jarl
18
不过,这种方法并不是万无一失的,尤其是当文件已更改并且很小的情况下。有没有一种方法可以明确告诉git文件的移动呢? - Rebs
19
如果Git在提交时检测到重命名,则查看该文件的历史记录将会显示旧文件名的历史记录,这不仅仅是一项表面上的功能,这正是将文件重命名与删除+添加区分开来的关键所在。 (在Git 2.20上进行了测试) - arberg
显示剩余9条评论

189

将移动和修改操作分别提交。


21
我知道Git可以同时处理移动和修改。在使用Java编程和IDE时,重命名一个类既是一次修改,也是一次移动。我了解Git甚至可以自动判断何时发生了移动(即删除和创建)。 - pupeno
11
@jrockway,我有过这种情况。对于小文件来说很容易发生,我猜它们“变得‘太不同’而不能被视为移动”。 - ANeves
12
需要注意的一点是,如果git diff在一个提交中无法识别重命名操作,则它在两个提交中也无法识别。您需要使用-M--find-renames选项来进行识别。所以,如果您提出这个问题的动机是为了在拉取请求中查看重命名操作(根据经验说),将其分成两个提交并不能更接近实现这个目标。 - mattliu
5
这个回答似乎缺乏深度。像GIT这样的高级版本控制系统应该有一种方法,在一个提交中将添加的文件设置为已删除文件的重命名。 - cdalxndr
7
Git 没有标记重命名的方法与这个答案的质量无关。 - KenIchi
显示剩余13条评论

55

git diff -Mgit log -M 应该会自动检测到那些发生了小改动的重命名操作,只要它们确实是小改动。 如果你的小改动不是那么小,可以减少相似度阈值,例如:

$ git log -M20 -p --stat

将其从默认的50%降低到20%。


21
可否在配置文件中定义阈值? - ReDetection
1
这是与 git log 单独提供的相同的“交互式窗口”,它允许在终端中滚动并使用 q 退出。别担心,在单独的 git log 视图中很难搞砸什么。 - theannouncer

46
这是一个快速而简单的解决方案,适用于一个或几个已重命名和修改但未提交的文件。
假设文件名为foo,现在改名为bar
  1. bar 重命名为临时名称:

    mv bar side
    
  2. 检出 foo

    git checkout HEAD foo
    
  3. 使用 Git 将 foo 重命名为 bar

    git mv foo bar
    
  4. 现在将您的临时文件重新命名为 bar

    mv side bar
    

这最后一步是将您更改的内容返回到文件中的步骤。

虽然这可能有效,但如果移动的文件与原始文件的内容差异太大,Git 将认为决定这是一个新对象更加高效。让我演示一下:

$ git status
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    renamed:    README -> README.md

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:   README.md
    modified:   work.js

$ git add README.md work.js # why are the changes unstaged, let's add them.
$ git status
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    deleted:    README
    new file:   README.md
    modified:   work.js

$ git stash # what? let's go back a bit
Saved working directory and index state WIP on dir: f7a8685 update
HEAD is now at f7a8685 update
$ git status
On branch workit
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    .idea/

nothing added to commit but untracked files present (use "git add" to track)
$ git stash pop
Removing README
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    new file:   README.md

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:    README
    modified:   work.js

Dropped refs/stash@{0} (1ebca3b02e454a400b9fb834ed473c912a00cd2f)
$ git add work.js
$ git status
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    new file:   README.md
    modified:   work.js

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:    README

$ git add README # hang on, I want it removed
$ git status
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    deleted:    README
    new file:   README.md
    modified:   work.js

$ mv README.md Rmd # Still? Try the answer I found.
$ git checkout README
error: pathspec 'README' did not match any file(s) known to git.
$ git checkout HEAD README # Ok the answer needed fixing.
$ git status
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    new file:   README.md
    modified:   work.js

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:    README.md
    modified:   work.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    Rmd

$ git mv README README.md
$ git status
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    renamed:    README -> README.md
    modified:   work.js

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:   work.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    Rmd

$ mv Rmd README.md
$ git status
On branch workit
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   .gitignore
    renamed:    README -> README.md
    modified:   work.js

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:   README.md
    modified:   work.js

$ # actually that's half of what I wanted; \
  # and the js being modified twice? Git prefers it in this case.

36
这个过程没有任何意义。Git对于重命名不添加任何元数据,git mv只是为了方便使用git rm/git add命令组合。如果您已经执行了'mv bar foo'操作,则只需确保在提交之前要执行git add foogit rm bar。这可以通过单个git add -A命令完成,或者可能需要使用git add foo; git commit -a两个命令来完成。 - CB Bailey
23
我知道的是,在我执行此操作之前,Git无法识别它为移动操作。但是在我执行此操作之后,Git就能够识别它为移动操作了。 - user186821
10
它将其识别为一次移动操作。但它现在也将更改视为未暂存的更改。一旦您再次add该文件,git会将移动/修改拆分为删除/再添加。 - Michael Piefel
6
如果你在第三步之后运行git commit,那么这个过程就可以正常工作,否则就是错误的。此外,@CharlesBailey是正确的,你同样可以在第三步时使用普通的mv blah foo命令,然后进行提交,得到相同的结果。 - DaFlame
6
"这个过程没有任何意义。"- 当Git混淆并认为一个文件已被删除,另一个文件已被添加时,此过程就有了作用。这将消除Git的困惑,并明确标记该文件已移动,而无需进行中间提交。 - Danack
显示剩余3条评论

46

这完全是个感性的问题。Git通常相当擅长识别移动操作,因为GIt是一个内容追踪器。

所有这些只取决于你的“stat”如何显示它。唯一的区别在于-M标志。

git log --stat -M

commit 9c034a76d394352134ee2f4ede8a209ebec96288
Author: Kent Fredric
Date:   Fri Jan 9 22:13:51 2009 +1300


        Category Restructure

     lib/Gentoo/Repository.pm                |   10 +++++-----
     lib/Gentoo/{ => Repository}/Base.pm     |    2 +-
     lib/Gentoo/{ => Repository}/Category.pm |   12 ++++++------
     lib/Gentoo/{ => Repository}/Package.pm  |   10 +++++-----
     lib/Gentoo/{ => Repository}/Types.pm    |   10 +++++-----
     5 files changed, 22 insertions(+), 22 deletions(-)

git log --stat

commit 9c034a76d394352134ee2f4ede8a209ebec96288
Author: Kent Fredric
Date:   Fri Jan 9 22:13:51 2009 +1300

    Category Restructure

 lib/Gentoo/Base.pm                |   36 ------------------------
 lib/Gentoo/Category.pm            |   51 ----------------------------------
 lib/Gentoo/Package.pm             |   41 ---------------------------
 lib/Gentoo/Repository.pm          |   10 +++---
 lib/Gentoo/Repository/Base.pm     |   36 ++++++++++++++++++++++++
 lib/Gentoo/Repository/Category.pm |   51 ++++++++++++++++++++++++++++++++++
 lib/Gentoo/Repository/Package.pm  |   41 +++++++++++++++++++++++++++
 lib/Gentoo/Repository/Types.pm    |   55 +++++++++++++++++++++++++++++++++++++
 lib/Gentoo/Types.pm               |   55 -------------------------------------
 9 files changed, 188 insertions(+), 188 deletions(-)

git help log


(翻译:获取git命令log的帮助信息)
   -M
       Detect renames.

   -C
       Detect copies as well as renames. See also --find-copies-harder.

108
如果我的说法显得有点小题大做,那我很抱歉。“Git is generally rather good at recognising moves, because GIT is a content tracker” 这句话似乎对我来说是个非关系论。没错,它是一个内容追踪器,也许它恰好擅长检测移动操作,但这两个陈述并没有必然联系。仅仅因为它是一个内容追踪器,并不意味着它的移动操作检测一定很好。实际上,一个内容追踪器可能根本就没有移动操作检测功能。 - Laurence Gonsalves
1
@WarrenSeine 当你重命名一个目录时,该目录中文件的SHA1值不会改变。你只有一个具有相同SHA1值的新TREE对象。重命名目录不会导致重大数据更改的原因。 - Kent Fredric
1
@KentFredric,您展示了使用“git log”和-M选项可以检查文件是否被重命名,我尝试了一下,它运行良好。但是当我将我的代码提交到审查并在Gerrit(https://www.gerritcodereview.com/)中查看该文件时,它显示该文件是新添加的,之前的文件已被删除。那么,在“git commit”中是否有选项可以使提交后Gerrit正确显示? - Patrick
2
不,我根本没有展示那个。我展示了Git能够假装添加+删除是由于内容相同而重命名的。它无法知道发生了什么,并且它并不关心。Git只记住“添加”和“删除”。 “重命名”从未被记录。 - Kent Fredric
1
例如,最近我在一个代码库上进行了很多“复制+编辑”的操作。大多数查看方式只能看到“新文件”,但是如果你通过git log -M1 -C1 -B1 -D --find-copies-harder命令,Git可以“发现”这个新文件可能是先前被复制的。有时它做得很对,但有时它会找到完全不相关的文件,这些文件恰好具有相同的内容。 - Kent Fredric
显示剩余7条评论

37
你可以试试Amber的回答另一个问题!再次引用它:

First, cancel your staged add for the manually moved file:

$ git reset path/to/newfile
$ mv path/to/newfile path/to/oldfile

Then, use Git to move the file:

$ git mv path/to/oldfile path/to/newfile

Of course, if you already committed the manual move, you may want to reset to the revision before the move instead, and then simply git mv from there.


10
请注意,如果您还进行了重大更改,则此方法无效,这通常发生在小文件上。它将被提交为“删除”和“创建”。在这种情况下,我想唯一的选择是按照另一个答案中建议的方式,分别进行移动和修改,然后进行单独的提交。 - Albert Vila Calvo
@AlbertVilaCalvo 你确定吗?你必须在提交之前做这个 最后 一步,也就是说,你不能运行 git mv,再进行编辑,然后让它生效,但我刚刚编辑了文件的 > 50%,并在一个提交中重命名/移动了它。换句话说,在执行 git mv 之前,你必须先编辑文件。 - undefined
@ruffin 首先使用 git mv oldfile newfile 命令,git status 会将更改注册为 modified: newfile,但是在执行 git add newfile 后,状态会变为 deleted: oldfilenew file: newfile。无论如何,这是正常的操作。 - undefined

24
如果你说的是 `git status` 没有显示重命名的内容,尝试使用 `git commit --dry-run -a` 代替。

11
如果你使用TortoiseGit,重命名检测是在提交时进行的,但这一点并不总是在软件之前显示。我将两个文件移动到另一个目录并进行了一些微小的编辑,我使用TortoiseGit作为我的提交工具,"Changes made"列表显示文件被删除和添加,而不是移动。从命令行运行 git status 显示相似情况。然而,在提交文件后,它们在日志中显示为已重命名。所以回答你的问题是,只要你没有做任何太过激烈的事情,Git应该会自动识别出重命名。编辑:显然,如果您添加了新文件,然后从命令行运行 git status,重命名应该会在提交之前显示。编辑2:此外,在TortoiseGit中,在提交对话框中添加新文件,但不要提交它们。然后,如果您进入 "Show Log" 命令并查看工作目录,您将看到 Git 是否在提交之前检测到了重命名。同样的问题在这里提出:https://tortoisegit.org/issue/1389,并已经记录为修复错误:https://tortoisegit.org/issue/1440。结果显示这是TortoiseGit提交对话框的显示问题,在您没有添加新文件时也存在于git status中。

2
你是对的,即使TortoiseGit显示删除+添加,即使git状态显示删除+添加,即使git commit --dry-run显示删除+添加,在git commit之后我看到的是重命名而不是删除+添加。 - Tomas Kubes
1
我非常确定自动重命名检测发生在历史记录检索期间;在提交中,它始终是添加+删除。这也解释了您所描述的分裂行为。因此,这里有选项:https://dev59.com/FnRC5IYBdhLWcg3wCMg6#434078 - Mark Sowul

11
其他答案已经涵盖了你可以简单地使用git add NEW && git rm OLD来使Git识别移动操作的内容。
然而,如果您已经修改了工作目录中的文件,则add+rm的方法将把修改添加到索引中,这在某些情况下可能是不希望的(例如,在有实质性修改的情况下,Git可能不再认为它是一个文件重命名)。
假设您想向索引中添加重命名,但不包括任何修改。实现此目的的明显方法是进行前后重命名:mv NEW OLD && git mv OLD NEW
但也有一种(稍微复杂一些)直接在索引中完成而不重命名工作目录中的文件的方法:
info=$(git ls-files -s -- "OLD" | cut -d' ' -f-2 | tr ' ' ,)
git update-index --add --cacheinfo "$info,NEW" &&
  git rm --cached "$old"

这也可以作为别名放入您的~/.gitconfig文件中:
[alias]
    mv-index = "!f() { \
      old=\"$1\"; \
      new=\"$2\"; \
      info=$(git ls-files -s -- \"$old\" | cut -d' ' -f-2 | tr ' ' ,); \
      git update-index --add --cacheinfo \"$info,$new\" && \
      git rm --cached \"$old\"; \
    }; f"

+1,遗憾的是,使用git mv没有直接实现这个功能的方法,就像使用--cached在git rm中一样。在我的情况下,我移动了整个目录,所以我使用了一个稍微不同的方法:git ls-files -s -- OLD |sed 's#\tOLD/#\tNEW/#',然后跟着使用git rm -r --cached OLD。注意:sed中的\t和尾随的/匹配路径前面的制表符和目录分隔符,请使用从您的repo根目录开始的完整路径(这确保您仅匹配该目录,而不是任何类似命名的文件/目录)。 - Thomas Guyot-Sionnest
为了完整起见,您应该展示如何使用mv-index - undefined

8

使用git mv命令移动文件,而不是操作系统的移动命令: https://git-scm.com/docs/git-mv

请注意,git mv命令仅在Git版本1.8.5及以上才存在。因此,您可能需要更新您的Git以使用此命令。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接