是否可以在Git中移动/重命名文件并维护它们的历史记录?

我想重命名/移动Git中的项目子树,将其从

/project/xyz

/components/xyz

如果我使用纯git mv project components,那么xyz project的所有提交历史记录都会丢失。有没有办法移动它以保持历史记录?

438663 次浏览
git log --follow [file]

将通过重命名向您展示历史。

Git检测重命名而不是使用提交持久化操作,因此您使用git mv还是mv并不重要。

log命令接受一个--follow参数,该参数在重命名操作之前继续历史记录,即它使用启发式搜索类似的内容。

要查找完整的历史记录,请使用以下命令:

git log --follow ./path/to/file

我做:

git mv {old} {new}git add -u {new}

重命名文件并保持历史记录不变是可能,尽管它会导致文件在存储库的整个历史记录中被重命名。这可能只适用于痴迷于git-log的人,并有一些严重的影响,包括:

  • 你可能正在重写共享历史,这是使用Git时最重要的不要。如果其他人克隆了存储库,你这样做会破坏它。他们将不得不重新克隆以避免头痛。如果重命名足够重要,这可能没关系,但你需要仔细考虑这一点-你最终可能会扰乱整个开源社区!
  • 如果您在存储库历史记录的早期使用它的旧名称引用了该文件,那么您实际上破坏了早期版本。为了解决这个问题,您必须做更多的跳圈。这不是不可能的,只是乏味,可能不值得。

现在,既然你还和我在一起,你可能是一个单独的开发人员,重命名一个完全隔离的文件。让我们使用filter-tree移动一个文件!

假设您要将文件old移动到文件夹dir并将其命名为new

这可以用git mv old dir/new && git add -u dir/new完成,但这打破了历史。

而是:

git filter-branch --tree-filter 'if [ -f old ]; then mkdir dir && mv old dir/new; fi' HEAD

Willredo分支中的每一次提交,在每次迭代的滴答声中执行命令。当你这样做的时候,很多东西都可能出错。我通常测试文件是否存在(否则它还没有移动),然后执行必要的步骤来按照我的喜好固定树。在这里,你可能会通过文件来更改对文件的引用等等。自力更生!:)

完成后,文件被移动,日志完好无损。你感觉自己像个忍者海盗。

此外;当然,只有当您将文件移动到新文件夹时,才需要mkdir目录。如果将避免在历史记录中早于您的文件存在之前创建此文件夹。

不。

简短的答案是。在Git中重命名文件并记住历史是不可能的。这很痛苦。

有传言说git log --follow --find-copies-harder会起作用,但它对我不起作用,即使文件内容的更改为零,并且已经使用git mv进行了移动。

(最初我使用Eclipse在一个操作中重命名和更新包,这可能会让Git感到困惑。但这是一件非常常见的事情。如果只执行mv,然后执行commitmv并不太远,那么--follow似乎确实有效。)

Linus说你应该全面理解软件项目的全部内容,而不需要跟踪单个文件。好吧,可悲的是,我的小大脑无法做到这一点。

这么多人盲目地重复Git自动跟踪移动的声明是真的很烦人。他们浪费了我的时间。Git没有这样做。根据设计(!)Git根本不跟踪移动。

我的解决方案是将文件重命名回它们的原始位置。更改软件以适应源代码控制。使用Git,您似乎只需要在第一次就“git”它。

不幸的是,这打破了Eclipse,它似乎使用--followgit log --follow有时不显示具有复杂重命名历史的文件的完整历史记录,即使git log显示了。(我不知道为什么。)

(有一些太聪明的黑客会回头重新开始旧工作,但它们相当可怕。参见GitHub-Gist:e Miller/g it-mv-with-历史。)

简而言之:如果Subversion这样做是错误的,那么Git这样做也是错误的-这样做不是什么(错误!)特性,这是个错误。

  1. 您使用git log --pretty=email将文件的提交历史记录转换为电子邮件补丁
  2. 您在新目录中重新组织这些文件并重命名它们
  3. 您将这些文件(电子邮件)转换回Git提交以使用git am保留历史记录。

限制

  • 标签和分支不保留
  • 在路径文件重命名(目录重命名)上剪切历史记录

一步一步的解释与例子

1.提取电子邮件格式的历史记录

示例:提取file3file4file5的历史记录

my_repo├── dirA│   ├── file1│   └── file2├── dirB            ^│   ├── subdir      | To be moved│   │   ├── file3   | with history│   │   └── file4   |│   └── file5       v└── dirC├── file6└── file7

设置/清理目的地

export historydir=/tmp/mail/dir       # Absolute pathrm -rf "$historydir"    # Caution when cleaning the folder

以电子邮件格式提取每个文件的历史记录

cd my_repo/dirBfind -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "$0" > "$historydir/$0"' {} ';'

不幸的是,选项--follow--find-copies-harder不能与--reverse组合。这就是为什么在文件重命名时(或重命名父目录时)会删除历史记录的原因。

电子邮件格式的临时历史记录:

/tmp/mail/dir├── subdir│   ├── file3│   └── file4└── file5

Dan Bonachea建议在第一步中反转git log生成命令的循环:不是每个文件运行一次git log,而是在命令行上使用文件列表运行它一次,并生成一个统一的日志。这样修改多个文件的提交在结果中保持单个提交,所有新的提交都保持其原始的相对顺序。请注意,当重写(现已统一)日志中的文件名时,这还需要在下面的第二步中进行更改。


2.重组文件树并更新文件名

假设您想在另一个存储库中移动这三个文件(可以是同一个存储库)。

my_other_repo├── dirF│   ├── file55│   └── file56├── dirB              # New tree│   ├── dirB1         # from subdir│   │   ├── file33    # from file3│   │   └── file44    # from file4│   └── dirB2         # new dir│        └── file5    # from file5└── dirH└── file77

因此,重新组织您的文件:

cd /tmp/mail/dirmkdir -p dirB/dirB1mv subdir/file3 dirB/dirB1/file33mv subdir/file4 dirB/dirB1/file44mkdir -p dirB/dirB2mv file5 dirB/dirB2

您的临时历史现在是:

/tmp/mail/dir└── dirB├── dirB1│   ├── file33│   └── file44└── dirB2└── file5

更改历史记录中的文件名:

cd "$historydir"find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'

3.应用新的历史

您的另一个repo是:

my_other_repo├── dirF│   ├── file55│   └── file56└── dirH└── file77

从临时历史文件应用提交:

cd my_other_repofind "$historydir" -type f -exec cat {} + | git am --committer-date-is-author-date

--committer-date-is-author-date保留原始提交时间戳(Dan Bonachea的注释)。

您的另一个repo现在是:

my_other_repo├── dirF│   ├── file55│   └── file56├── dirB│   ├── dirB1│   │   ├── file33│   │   └── file44│   └── dirB2│        └── file5└── dirH└── file77

使用git status查看准备推送的提交量:-)


额外技巧:检查存储库中的重命名/移动文件

要列出已重命名的文件:

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'

更多自定义:您可以使用选项--find-copies-harder--reverse完成命令git log。您还可以使用cut -f3-和greping完整模式'{.* => .*}'删除前两列。

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'

虽然Git的核心,Git管道不跟踪重命名,但如果您愿意,您使用Git日志“瓷器”显示的历史记录可以检测到它们。

对于给定的git log,使用-M选项:

git log-p-m使用场景

使用当前版本的Git。

这也适用于git diff等其他命令。

有一些选项可以使比较更加严格或不那么严格。如果您重命名文件而不同时对文件进行重大更改,则Git日志和朋友更容易检测到重命名。出于这个原因,有些人在一次提交中重命名文件并在另一次提交中更改它们。

每当您要求Git查找文件重命名的位置时,CPU使用都会产生成本,因此您是否使用它以及何时使用它取决于您。

如果您希望始终在特定存储库中使用重命名检测报告您的历史记录,您可以使用:

git configdiff.renames1

检测到文件从一个目录移动到另一个。这是一个例子:

commit c3ee8dfb01e357eba1ab18003be1490a46325992Author: John S. Gruber <JohnSGruber@gmail.com>Date:   Wed Feb 22 22:20:19 2017 -0500
test rename again
diff --git a/yyy/power.py b/zzz/power.pysimilarity index 100%rename from yyy/power.pyrename to zzz/power.py
commit ae181377154eca800832087500c258a20c95d1c3Author: John S. Gruber <JohnSGruber@gmail.com>Date:   Wed Feb 22 22:19:17 2017 -0500
rename test
diff --git a/power.py b/yyy/power.pysimilarity index 100%rename from power.pyrename to yyy/power.py

请注意,每当您使用diff时,这都有效,而不仅仅是git log。例如:

$ git diff HEAD c3ee8dfdiff --git a/power.py b/zzz/power.pysimilarity index 100%rename from power.pyrename to zzz/power.py

作为试验,我在功能分支的一个文件中做了一个小的更改并提交了它,然后在主分支中重命名了文件,提交了,然后在文件的另一部分做了一个小的更改并提交了它。当我去功能分支并从主分支合并时,合并重命名了文件并合并了更改。这是合并的输出:

 $ git merge -v masterAuto-merging singleMerge made by the 'recursive' strategy.one => single | 4 ++++1 file changed, 4 insertions(+)rename one => single (67%)

结果是一个工作目录,文件重命名并进行了两次文本更改。因此,尽管Git没有显式跟踪重命名,但它仍有可能做正确的事情。

这是对旧问题的迟到回答,因此其他答案可能在当时的Git版本中是正确的。

我想重命名/移动Git中的项目子树,将其从

/project/xyz

/components/xyz

如果我使用普通的git mv project components,那么xyz项目的所有提交历史都会丢失。

不会(8年后,Git 2.19,Q3 2018),因为Git将检测目录重命名,现在这是更好的记录。

请参阅提交b00bf1c提交1634688提交0661e494d34dff提交提交983f464提交c840e1a提交9929430(2018年6月27日)和提交d4e8062提交5dacd4a(2018年6月25日)by提交b00bf1c0。
(由Junio C Hamano----#0----合并于提交0ce5a69,2018年7月24日)

现在在Documentation/technical/directory-rename-detection.txt中解释:

示例:

当所有x/ax/bx/c都移动到z/az/bz/c时,很可能同时添加的x/d也想移动到z/d提示整个目录“x”移动到“z”。

但还有许多其他情况,例如:

历史的一边重命名x -> z,另一边重命名一些文件为x/e,导致合并需要执行传递重命名。

为了简化目录重命名检测,这些规则由Git强制执行:

一些基本规则限制了什么时候目录重命名检测适用:

  1. 如果一个给定的目录仍然存在于合并的两边,我们不认为它已被重命名。
  2. 如果要重命名的文件的子集有一个文件或目录的方式(或将在彼此的方式),“关闭”这些特定子路径的目录重命名并将冲突报告给用户。
  3. 如果历史记录的另一边将目录重命名为您的历史记录的另一边重命名的路径,则忽略历史记录的另一边的特定重命名,以进行任何隐式目录重命名(但警告用户)。

您可以在t/t6043-merge-rename-directories.sh中看到很多的测试,其中还指出:

  • a)如果重命名将一个目录拆分为两个或多个其他目录,重命名最多的目录“获胜”。
  • b)避免路径的目录重命名检测,如果该路径是合并两侧重命名的来源。
  • c)只有在另一边的情况下才对目录应用隐式目录重命名历史是重命名的人。

首先创建一个只有重命名的独立提交。

然后将对文件内容的任何最终更改放入单独的提交中。

重命名目录或文件(我对复杂情况了解不多,所以可能会有一些警告):

git filter-repo --path-rename OLD_NAME:NEW_NAME

在提到它的文件中重命名目录(可以使用回调,但我不知道如何):

git filter-repo --replace-text expressions.txt

expressions.txt是一个充满literal:OLD_NAME==>NEW_NAME这样的行的文件(可以将Python的RE与regex:一起使用,也可以将Glob与glob:一起使用)。

要在提交消息中重命名目录:

git-filter-repo --message-callback 'return message.replace(b"OLD_NAME", b"NEW_NAME")'

也支持Python的正则表达式,但它们必须用Python手动编写。

如果存储库是原始的,没有远程,则必须添加--force才能强制重写。(您可能希望在执行此操作之前创建存储库的备份。)

如果您不想保留引用(它们将显示在Git GUI的分支历史记录中),则必须添加--replace-refs delete-no-add

我遵循这个多步骤过程将代码移动到父目录并保留历史记录。

步骤0:从“master”创建一个分支“历史”以进行安全保存

第1步:使用git-filter-repo开发完成工具重写历史记录。下面的此命令将文件夹FolderWith ContentOfinterest移动到一个级别并修改了相关的提交历史记录

git filter-repo --path-rename ParentFolder/FolderwithContentOfInterest/:FolderwithContentOfInterest/ --force

第2步:此时GitHub存储库丢失了其远程存储库路径。添加了远程引用

git remote add origin git@github.com:MyCompany/MyRepo.git

步骤3:提取存储库上的信息

git pull

步骤4:将本地丢失的分支连接到原始分支

git branch --set-upstream-to=origin/history history

第5步:如果出现提示,文件夹结构的地址合并冲突

第6步:推送!!

git push

注意:修改的历史记录和移动的文件夹似乎已经提交。enter code here

完成。代码移动到父/所需目录,保持历史完整!

简单地移动文件和阶段:

git add .

在提交之前,您可以检查状态:

git status

这将显示:

Changes to be committed:(use "git restore --staged <file>..." to unstage)renamed:    old-folder/file.txt -> new-folder/file.txt

我使用Git版本2.26.1进行了测试。

GitHub帮助页面中提取。

我遇到了问题"重命名文件夹而不会丢失历史记录"。要修复它,请运行:

$ git mv oldfolder temp && git mv temp newfolder$ git commit$ git push