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

/project/xyz


移至

/components/xyz


如果我使用普通git mv project components,那么所有xyz project的提交历史都将丢失。有什么办法可以保持历史记录吗?

评论

关于git mv:stackoverflow.com/questions/1094269/whats-the-purpose-of-git-mv

我只想指出,我只是通过文件系统测试了移动文件,并且在提交(通过intellij)后,可以查看历史记录(同样在intellij中),从而可以看到整个历史记录(包括位于不同位置的历史记录)。我假设intellij并没有做任何特别的事情,所以很高兴知道至少可以追溯到历史。

有关检测目录重命名时Git遵循的规则,请参见下面的答案

我在这里写了一个答案。我希望它能起作用。 stackoverflow.com/questions/10828267/…

#1 楼

Git会检测到重命名,而不是通过提交来持久化操作,因此无论使用git mv还是mv都没有关系。使用试探法搜索相似的内容:

http://git-scm.com/docs/git-log

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

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


评论


我怀疑这是性能方面的考虑。如果不需要完整的历史记录,则扫描内容肯定需要更长的时间。最简单的方法是设置一个别名git config alias.logf“ log --follow”并只将git logf写入./path/to/file。

–汤姆森(Troels Thomsen)
2010-2-23在7:36

@TroelsThomsen来自此答案的Linus Torvalds的电子邮件表明,这是Git的故意设计选择,因为据称它比跟踪重命名等功能强大得多。

–埃米尔·伦德伯格(Emil Lundberg)
2013年9月6日下午14:58

这个答案有点误导。 Git确实可以“检测到重命名”,但是在游戏后期却很晚。问题是询问您如何确保Git跟踪重命名,而阅读此文件的人可以轻松推断Git会自动为您检测到并记录下来。它不是。 Git并没有真正处理重命名,而是有合并/日志工具试图找出发生的事情-很少能正确解决。 Linus对于为什么git绝不应该以正确的方式进行操作并明确跟踪重命名有一个错误但强烈的争论。所以,我们被困在这里。

–克里斯·莫斯基尼(Chris Moschini)
2014年4月10日在18:19



重要提示:例如,在重命名Java软件包期间,如果要重命名目录,请确保执行两次提交,首先执行'git mv {old} {new}'命令,第二次执行引用该Java文件的所有Java文件的更新。更改软件包目录。否则,即使使用--follow参数,git也无法跟踪单个文件。

– nn4l
14-10-4在16:31



尽管Linus可能犯了很少的错误,但这确实是一个错误。简单地重命名文件夹会导致大量增量上传到GitHub。这使我在重命名文件夹时非常谨慎...但这对程序员来说是个很大的负担。有时,我必须重新定义事物的含义,或更改事物的分类方式。莱纳斯:“换句话说,我是对的。我总是对的,但有时候我比其他时候更对。而且,该死,当我说'文件无关紧要'时,我真的是真的( Tm值)。” ...我对此感到怀疑。

–Gabe Halsmer
2015年9月11日在16:04

#2 楼

不。
简单的回答是不。不能在Git中重命名文件并记住历史记录。
谣传git log --follow --find-copies-harder可以工作,但即使文件内容更改为零,并且对git mv进行了移动,它也对我不起作用。
(最初,我使用Eclipse在一个操作中重命名和更新软件包,这可能会使Git感到困惑。但这是很常见的事情。如果仅执行--follow然后执行mvcommit,则mv确实可以正常工作。不太远。)
Linus说,您应该全面了解软件项目的全部内容,而无需跟踪单个文件。好吧,可悲的是,我的大脑无法做到这一点。
真是令人讨厌的是,这么多人不知不觉地重复了Git自动跟踪动作的说法。他们浪费了我的时间。 Git没有这样的事情。根据设计(!),Git根本不会跟踪移动。
我的解决方案是将文件重命名为其原始位置。更改软件以适合源代码管理。使用Git,您似乎只需要在第一时间对其进行“ git”处理。
不幸的是,这破坏了Eclipse,后者似乎使用了--follow。即使git log --follow有时,git log有时也不显示具有复杂重命名历史记录的文件的完整历史记录。 (我不知道为什么。)
(有一些非常聪明的hacks可以返回并重新提交旧工作,但是它们相当可怕。请参阅GitHub-Gist:emiller / git-mv-with-history。)

评论


我相信你是对的。我只是想使用php-cs-fixer重新格式化Laravel 5项目的源,但它坚持要更改名称空间子句的大小写以匹配app文件夹的小写值。但是名称空间(或作曲家自动加载)仅适用于CamelCase。我需要将文件夹的大小写更改为App,但这会使我的更改丢失。这是最琐碎的示例,但显示了git启发式方法如何也无法遵循最简单的名称更改(--follow和--find-copies-harder应该是规则,而不是例外)。

–扎克·莫里斯(Zack Morris)
2016年7月9日在3:25



git -1,subversion +1

–cosmin
17-10-11在9:40

这是真的吗?这是我现在暂时不使用tfs的更多原因,在大型项目中必须保留移动/重命名文件的历史记录。

– es撒
19-10-5的1:36

简短的回答是。 Git当前版本也支持“ git log --follow”。我同意@MohammadDehghan

–insung
1月22日在1:38



git log --follow对我有用,但仅当git mv将文件移动到未跟踪的文件时。如果您尝试执行rm a.txt和&git mv b.txt a.txt,则b.txt的历史记录将被销毁。如果要使git log --follow正常工作,则必须先git rm a.txt然后提交,然后是git mv b.txt a.txt。

–吉莱斯皮
9月30日23:02



#3 楼

可以重命名文件并保持历史记录完整,尽管这会导致在存储库的整个历史记录中重命名文件。这可能仅适用于痴迷的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


将重做分支中的每个提交,并在每次迭代的滴答处执行命令。当您这样做时,很多东西都会出错。我通常会测试文件是否存在(否则,文件尚不存在),然后执行必要的步骤来拔出我喜欢的树。在这里,您可能浏览文件以更改对该文件的引用,依此类推。把自己打昏! :)

完成后,文件将移动并且日志完整无缺。你觉得自己像个忍者海盗。

当然,仅当您将文件移动到新文件夹时,才需要mkdir dir。如果使用if,则会避免在历史记录中早于文件存在创建此文件夹。

评论


作为一个沉迷的git-log-lover,我不会这么做。这些文件在当时没有被命名,因此历史反映了从未存在过的情况。谁知道过去可能会失败的测试!在每种情况下,破坏早期版本的风险几乎都不值得。

– Vincent
2014年1月6日在16:52

@Vincent你是绝对正确的,我试图尽可能清楚地知道此解决方案是否合适。在这种情况下,我还认为我们在谈论“历史”一词的两种含义,我对此表示赞赏。

–Øystein Steimler
2014年1月7日13:41



我发现有些情况下可能需要这样做。假设我在自己的个人分支中开发了一些东西,现在我想将其合并到上游。但是我发现文件名不合适,因此我将其更改为整个个人分支。这样,我可以保留整洁的正确历史记录,并从一开始就使用正确的名称。

–user2291758
2015年3月4日在7:52

@ user2291758这是我的用例。这些功能更强大的git命令很危险,但这并不意味着如果您知道自己在做什么,它们就没有非常引人注目的用例!

– philix
16年9月1日在12:06

如果可能的话,使用--index-filter进行重命名将更快,因为不必在每次提交时都将树检出并返回。 --index-filter直接作用于每个提交索引。

–托马斯·盖奥特·西内斯特(Thomas Guyot-Sionnest)
17年4月6日在14:03

#4 楼

git log --follow [file]


将通过重命名显示历史。

评论


看来这要求您在开始修改文件之前仅提交重命名。如果您移动文件(在外壳中)然后进行更改,则所有选择均关闭。

– yoyo
2011-12-13 22:15

@yoyo:这是因为git不跟踪重命名,而是检测到它们。 git mv基本上是git rm && git add。可以使用-M90 / --find-renames = 90之类的选项来考虑在文件90%相同时要重命名的文件。

–vdboor
2012年6月13日12:29



#5 楼

我愿意:

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


评论


-u似乎对我没有任何作用,是否应该更新历史记录?

–杰里米
13年1月24日在22:47

也许您想要-A的行为呢?同样,请参见此处:git-scm.com/docs/git-add

–詹姆斯·格林(James M. Greene)
13年1月26日在1:05

它确实添加了文件,但是不更新历史记录,因此“ git log file name”显示完整的历史记录。如果您仍然使用--follow选项,则仅显示完整的历史记录。

–杰里米
13年1月26日在1:45

我做了一个复杂的重构,移动了一个include目录(使用mv,而不是git mv),然后在重命名的文件中更改了许多#include路径。 git找不到足够的相似性来跟踪历史记录。但是git add -u只是我需要的东西。 git status现在指示“重命名”,之前显示“已删除”和“新文件”。

– AndyJost
2013年12月29日下午3:48

SO上有很多问题可以解决git add -u的目的。 Git文档往往无济于事,是我最后要看的地方。这是一篇显示git add -u的帖子:stackoverflow.com/a/2117202。

–布伦特·布拉德本(Brent Bradburn)
17年1月6日在17:00

#6 楼


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

/project/xyz


移至

/ components / xyz

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


否(8年后,Git 2.19,Q3 2018) ,因为Git会检测到目录重命名,并且现在对此进行了更好的记录。 ,并提交d4e8062,由Elijah Newren(newren)提交5dacd4a(2018年6月25日)(由Junio C Hamano-gitster合并-在commit 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,这导致合并需要进行可传递的重命名。


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

应用
目录重命名时的一些基本规则限制:



如果给定目录仍然存在在合并的两面,我们不认为它已被重命名。
如果要重命名的文件的子集在文件方式或目录中具有(或可能彼此相同)文件或目录,“关闭”目录重命名这些特定的子路径,并将冲突报告给用户。
如果历史记录的另一侧将目录重命名为您的历史记录已重命名的路径,则对于任何隐式目录重命名,请从历史记录的另一侧忽略该特定重命名(但警告用户)。



您可以在x/e中看到很多测试,这些测试还指出:



a)如果重命名,则将目录拆分为
b)避免路径的目录重命名检测,如果该路径是合并任一侧重命名的源。
/> c)如果历史的另一面是进行重命名的,则仅将隐式目录重命名应用于目录。



#7 楼



您使用git log --pretty=email将文件的提交历史记录转换为电子邮件补丁

您将这些文件重新组织到新目录中并重命名它们
您将这些文件转换回(电子邮件)到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 path
rm -rf "$historydir"    # Caution when cleaning the folder

以电子邮件格式提取每个文件的历史记录
cd my_repo/dirB
find -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "
/tmp/mail/dir
    ├── subdir
    │   ├── file3
    │   └── file4
    └── file5
" > "$historydir/
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
"' {} ';'

很遗憾,选项--follow--find-copies-harder无法与--reverse结合使用。这就是为什么重命名文件(或重命名父目录)时会剪切历史记录的原因。电子邮件格式的临时历史记录:
cd /tmp/mail/dir
mkdir -p dirB/dirB1
mv subdir/file3 dirB/dirB1/file33
mv subdir/file4 dirB/dirB1/file44
mkdir -p dirB/dirB2
mv file5 dirB/dirB2

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

2。重新组织文件树并更新文件名
假设您想将这三个文件移动到另一个仓库中(可以是相同的仓库)。
br />您的临时历史记录现在为:
/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]\)/[^ ]*:/
my_other_repo
├── dirF
│   ├── file55
│   └── file56
└── dirH
    └── file77
:g" -i "
cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am --committer-date-is-author-date
"' {} ';'


3。应用新的历史记录
您的其他存储库是:
my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB
│   ├── dirB1
│   │   ├── file33
│   │   └── file44
│   └── dirB2
│        └── file5
└── dirH
    └── file77

从临时历史记录文件中应用提交:
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'

--committer-date-is-author-date保留原始的提交时间戳(Dan Bonachea的评论) 。
您的其他仓库现在是:
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'

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

其他技巧:检查仓库中重命名/移动的文件
要列出已重命名的文件:
q4312078q
更多自定义项:可以使用选项git log--find-copies-harder完成命令--reverse。您还可以使用cut -f3-删除前两列,并grepping完整模式'{.* => .*}'
q4312078q

评论


注意:此技术将更改2个或多个文件的提交拆分为单独的碎片提交,并通过按文件名排序来扰乱它们的顺序(因此,一个原始提交的片段在线性历史记录中不会相邻出现)。因此,所得到的历史记录只是逐个文件“正确”的。如果要移动多个文件,则结果历史记录中没有新的提交表示原始回购记录中存在的已移动文件的一致快照。

– Dan Bonachea
16年11月15日17:00

嗨@DanBonachea。感谢您的有趣反馈。我已经使用这种技术成功迁移了一些包含多个文件的存储库(即使使用重命名的文件和跨目录移动的文件)。您建议对此答案进行哪些更改。您是否认为我们应该在此答案的顶部添加一个警告标语,以说明该技术的局限性?干杯

– oHo
16年11月16日在15:12

我通过在步骤1中反转git log generation命令的循环来适应该技术,从而避免了该问题。与其每个文件运行git log一次,不如在命令行上使用文件列表运行一次,并生成一个统一的日志。这样,修改2个或更多文件的提交在结果中仍然是单个提交,并且所有新提交都保持其原始相对顺序。请注意,在(现在已统一)日志中重写文件名时,这也需要在步骤2中进行更改。我还使用了git am --committer-date-is-author-date来保留原始的提交时间戳。

– Dan Bonachea
16 Dec 4'在3:44



感谢您的试验和分享。我为其他读者提供了一些答案。但是,我花了一些时间来测试您的处理过程。如果要提供命令行示例,请随时编辑此答案。干杯;)

– oHo
16 Dec 5'在6:08

#8 楼

我按照这个多步骤过程将代码移至父目录并保留了历史记录。

步骤0:从“ master”创建分支“ history”以进行保管

Step 1:使用git-filter-repo工具重写历史记录。此命令下面的命令将文件夹“ FolderwithContentOfInterest”上移到一个级别,并修改了相关的提交历史记录。步骤2:到此时GitHub存储库已丢失其远程存储库路径。添加了远程参考

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


步骤3:在存储库中提取信息

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


步骤4:连接本地带有原始分支的丢失分支

git pull


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

步骤6:推送! br />
git branch --set-upstream-to=origin/history history


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

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

评论


在答案列表中,这应该更高一些,因为从2020年起,filter-repo是进行此类操作的方法。

– mprost
9月18日晚上10:37

#9 楼

我已经遇到了“在不丢失历史记录的情况下重命名文件夹”的问题。要修复它,请运行:
 $ git mv oldfolder temp && git mv temp newfolder
$ git commit
$ git push
 


评论


这应该标记为正确答案。我完全可以将文件从同一存储库中的一个文件夹移动到另一个文件夹。我什至不必做“临时”的事情。 git mv olddir / file newdir / file对我有用。

–约翰·利弗莫尔
8月20日15:37

并保存所有历史记录。

–约翰·利弗莫尔
8月20日15:45

#10 楼

重命名目录或文件(对于复杂的情况我不太了解,因此可能有一些警告):

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

#11 楼

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

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


git log -p -M-


使用当前版本的Git。

也适用于其他命令,例如git diff

有一些选项可以使比较比较严格。如果您在重命名文件的同时未对文件进行重大更改,则Git日志和朋友可以更轻松地检测到重命名。因此,有些人会在一个提交中重命名文件,然后在另一个提交中进行更改。

,每当您要求Git查找文件重命名的位置时,CPU的使用都会产生成本,因此无论是否使用它,

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


git config diff.renames 1


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

commit c3ee8dfb01e357eba1ab18003be1490a46325992
Author: 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.py
similarity index 100%
rename from yyy/power.py
rename to zzz/power.py

commit ae181377154eca800832087500c258a20c95d1c3
Author: 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.py
similarity index 100%
rename from power.py
rename to yyy/power.py


请注意,无论您使用的是diff还是git log,它都可以使用。例如:

$ git diff HEAD c3ee8df
diff --git a/power.py b/zzz/power.py
similarity index 100%
rename from power.py
rename to zzz/power.py


作为一个试用版,我在功能分支中对一个文件进行了少量更改,然后提交了它,然后在主分支中,我将文件重命名并提交了,然后在文件的另一部分进行了少量更改并提交了。当我转到功能分支并从母版进行合并时,合并重命名了文件并合并了更改。这是合并的输出:

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


结果是一个工作目录,文件已重命名,并且两个文本都进行了更改。因此,尽管Git并未明确跟踪重命名,但Git仍可能做正确的事情。

这是对旧问题的较晚答案,因此其他答案对于Git可能是正确的当时的版本。

#12 楼

首先创建一个仅需重命名的独立提交。

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

#13 楼

只需移动文件并使用以下命令登台:

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帮助页面提取。

#14 楼

我先移动文件,然后执行

git add -A


,该文件将所有已删除/新的文件都放入存储区。 git意识到文件已移动。

git commit -m "my message"
git push


我不知道为什么,但这对我有用。

评论


这里的技巧是,您不必更改单个字母,即使您更改了某些内容并按Ctrl + Z,历史记录也会被破坏。因此,在这种情况下,如果您编写了一些内容,则会还原该文件,然后再次移动它,并对其进行单个add-> commit。

–西安
17年1月20日在8:56