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


667

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

/project/xyz

/components/xyz

如果我使用plain git mv project components,则xyz project丢失的所有提交历史记录。有什么办法可以移动历史记录吗?



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

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

我在这里写了一个答案。我希望它能起作用。stackoverflow.com/questions/10828267/...
马合木提埃菲社

Answers:


651

Git会检测到重命名,而不是通过提交保留操作,因此无论您使用git mv还是mv不重要。

log命令采用的--follow参数在重命名操作之前延续历史记录,即,它使用试探法搜索相似的内容:

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

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

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

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

13
@TroelsThomsen 这封电子邮件由Linus Torvalds,从链接这样的回答,表明它是一个有意的设计选择的Git的,因为它比据称跟踪重命名等功能更加强大
埃米尔·伦德伯格

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

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

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

94

可能重命名文件,并保持完好的历史,但它会导致整个仓库的整个历史要重命名的文件。这可能仅适用于痴迷的git-log-lovers,并具有一些严重的含义,包括:

  • 您可能正在重写共享历史记录,这是使用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。该如果将避免在历史上较早的该文件夹的创建比你的文件存在。


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

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

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

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

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

87

没有。

简短的答案是“ 否”。不能在Git中重命名文件并记住历史记录。这是一种痛苦。

谣言说它git log --follow--find-copies-harder可以工作,但是即使文件内容的更改为零,并且使用进行了移动,它也对我不起作用git mv

(最初,我使用Eclipse在一个操作中重命名和更新软件包,这可能会使Git感到困惑。但这是很常见的事情。--follow如果仅mv执行a然后执行a commit并且the mv距离不太远,似乎确实可行。)

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

如此多的人无意识地重复了Git自动跟踪移动的说法,这真是令人讨厌。他们浪费了我的时间。Git没有这样的事情。根据设计(!),Git根本不会跟踪移动。

我的解决方案是将文件重命名为其原始位置。更改软件以适合源代码管理。使用Git,您似乎只需要在第一时间正确进行“ git”操作即可。

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

(有一些非常聪明的hacks可以追溯到以前并重新提交旧的工作,但是它们相当令人恐惧。请参阅GitHub-Gist:emiller / git-mv-with-history。)


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

6
git -1,subversion +1
Cosmin

这是真的吗?这是我现在仍然使用tfs的更多原因,在大型项目中必须保留移动/重命名文件的历史记录。
塞萨尔(Cesar)

@Cesar如果说“记住历史”,他的意思是“查看日志时遵循重命名”(这是我们应该关心的唯一有用的事情),那么这从来不是真的!Git不会“记录”重命名,但是工具可以轻松地检测到它们并向我们展示重命名和移动。如果某人“不起作用”,则他/她应更改其正在使用的工具。有许多出色的Git GUI都具有此功能。
Mohammad Dehghan

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

42
git log --follow [file]

将通过重命名显示历史记录。


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

22
@yoyo:这是因为git不跟踪重命名,而是检测到它们。一个git mv基本上做一个git rm && git add。有类似-M90/的选项--find-renames=90,可以在文件90%相同时考虑重命名该文件。
vdboor 2012年

22

我做:

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

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

1
也许您想要-A代替它的行为?同样,请参见此处:git-scm.com/docs/git-add
James M. Greene

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

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

1
关于SO的目的有很多问题git add -u。Git文档往往无济于事,并且是我最后要看的地方。这是一个正在展示的帖子git add -ustackoverflow.com/a/2117202
nobar

17

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

/project/xyz

/ components / xyz

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

否(8年后,Git 2.19,2018年第三季度),因为Git将检测目录更名,现在对此进行了更好的记录。

请参阅Elijah Newren()的提交b00bf1c提交1634688提交0661e49提交4d34dff提交983f464提交c840e1a提交9929430(2018年6月27日)和提交d4e8062提交5dacd4a(2018年6月25日(由Junio C Hamano合并--commit 0ce5a69中,2018年7月24日)newren
gitster

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

例:

当所有的x/ax/bx/c已经转移到z/az/b而且z/c,很可能x/d在此期间加入也想搬到z/d采取了整个目录“的提示x”搬到“ z”。

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

历史记录的一侧重命名x -> z,而另一侧将某些文件重命名为 x/e,从而导致合并需要进行传递重命名。

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

应用目录重命名检测时有几个基本规则限制:

  1. 如果给定目录仍然存在于合并的两侧,则我们不认为该目录已被重命名。
  2. 如果要重命名的文件的子集有一个文件或目录(或互相干扰),请“关闭”这些特定子路径的目录重命名并将冲突报告给用户。
  3. 如果历史记录的另一侧确实将目录重命名为您的历史记录的另一侧重命名的路径,则对于任何隐式目录重命名,请从历史记录的另一侧忽略该特定重命名(但警告用户)。

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

  • a)如果重命名将目录分为两个或多个其他目录,则重命名最多的目录为“ wins”。
  • b)避免对目录进行目录重命名检测,如果该路径是合并任一侧的重命名源。
  • c)如果历史的另一端是进行重命名的人员,则仅将隐式目录重命名应用于目录。

15

目的

  • 使用(从Smar启发,从Exherbo借用git am
  • 添加复制/移动文件的提交历史记录
  • 从一个目录到另一个
  • 或者从一个存储库到另一个存储库

局限性

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

摘要

  1. 使用以下格式提取电子邮件格式的历史记录
    git log --pretty=email -p --reverse --full-index --binary
  2. 重组文件树并更新文件名
  3. 使用附加新的历史记录
    cat extracted-history | git am --committer-date-is-author-date

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 -- "$0" > "$historydir/$0"' {} ';'

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

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

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

Dan Bonachea建议在第一步中反转git log generation命令的循环:与其每个文件运行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/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

您的临时历史记录现在为:

/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.应用新的历史记录

您的其他回购是:

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

应用来自临时历史记录文件的提交:

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

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

您的其他仓库现在是:

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 '=>'

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

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

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

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

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

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

4

我遵循了此多步骤过程,将代码移至父目录和保留的历史记录。

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

步骤1:使用git-filter-repo工具重写历史记录。下面的此命令将文件夹“ FolderwithContentOfInterest”上移到一个级别,并修改了相关的提交历史记录

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

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


2

尽管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还是仅使用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版本可能是正确的。



1

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

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

只需移动文件并执行以下操作即可:

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


-3

我先移动文件,然后再做

git add -A

它将所有已删除/新文件放入分区区域。git意识到文件已移动。

git commit -m "my message"
git push

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


这里的窍门是,您不必更改单个字母,即使您更改了某些内容并按Ctrl + Z,历史记录也会被破坏。因此,在这种情况下,如果您编写了一些内容,则会还原该文件,然后再次移动它,并对其进行单个add-> commit。
Xelian
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.