【问题标题】:Is it possible to move/rename files in Git and maintain their history?是否可以在 Git 中移动/重命名文件并维护它们的历史记录?
【发布时间】:2023-12-17 02:16:01
【问题描述】:

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

/project/xyz

/components/xyz

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

【问题讨论】:

  • 我只想指出,我只是通过文件系统测试了移动文件,并且在提交(通过 intellij)之后,我可以在查看时看到整个历史记录(包括在不同位置时的历史记录)历史(再次在 intellij 中)。我假设 intellij 没有为此做任何特别特别的事情,所以很高兴知道至少可以追溯历史。
  • Git在检测目录重命名时遵循的规则见my answer below
  • 我在这里写了一个答案。我希望它有效。 *.com/questions/10828267/…
  • Git 子树无论如何都有“假”历史。当您使用git-subtree 分解存储库时,Git 会为生成的子树提供一个虚构的历史,该历史与它所分离的项目的历史不同。我相信 git 会尝试确定涉及子树中任何文件的所有提交,并使用它们将历史拼接在一起。此外,每次重新组合和重新拆分子树时,这些历史记录都会被重写。然而,每个子模块都有自己独立于父项目的历史记录。

标签: git rename mv


【解决方案1】:

没有。

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

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

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

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

真的很烦,这么多人盲目地重复 Git 自动跟踪移动的说法。他们浪费了我的时间。 Git 不会做这样的事情。 By design(!) Git does not track moves at all.

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

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

(有一些非常聪明的 hack 可以返回并重新提交旧工作,但它们相当可怕。请参阅 GitHub-Gist:emiller/git-mv-with-history。)

简而言之:如果Subversion doing this 是错误的,那么 Git 这样做也是错误的 - 这样做不是一些(错误!)功能,这是一个错误。

【讨论】:

  • 我相信你是对的。我只是想使用 php-cs-fixer 重新格式化我的 Laravel 5 项目的源代码,但它坚持要更改命名空间子句的大小写以匹配 app 文件夹的小写值。但命名空间(或作曲家自动加载)仅适用于 CamelCase。我需要将文件夹的大小写更改为 App 但这会导致我的更改丢失。这是最简单的示例,但显示了 git 启发式如何无法遵循即使是最简单的名称更改(--follow 和 --find-copies-harder 应该是规则,而不是例外)。
  • git -1,颠覆 +1
  • 这仍然是真的吗?这就是我现在继续使用 tfs 的更多理由,在大型项目中必须保留移动/重命名文件的历史记录。
  • 简短的回答是肯定的。 Git 当前版本也支持“git log --follow”。我同意@MohammadDehghan
  • 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
【解决方案2】:

是的

  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 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 生成命令的循环:与其对每个文件运行一次 git log,不如在命令行上使用文件列表只运行一次并生成单个统一日志.这样,修改多个文件的提交在结果中仍然是一个提交,并且所有新提交都保持其原始的相对顺序。请注意,在(现在统一的)日志中重写文件名时,这也需要在下面的第二步中进行更改。


2。重新组织文件树并更新文件名

假设您想将这三个文件移动到另一个 repo 中(可以是同一个 repo)。

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 查看准备推送的提交数量 :-)


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

列出已重命名的文件:

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

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

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

【讨论】:

  • 注意:此技术将更改 2 个或多个文件的提交拆分为单独的分段提交,并通过对文件名进行排序来打乱它们的顺序(因此一个原始提交的片段不会在线性历史中出现相邻)。因此,生成的历史记录仅在逐个文件的基础上是“正确的”。如果您要移动多个文件,则结果历史记录中没有新提交代表原始存储库历史记录中曾经存在的已移动文件的一致快照。
  • 嗨@DanBonachea。感谢您提供有趣的反馈。我已经使用这种技术成功迁移了一些包含多个文件的存储库(即使重命名的文件和跨目录移动的文件)。你建议在这个答案中改变什么。你认为我们应该在这个答案的顶部添加一个警告横幅来解释这种技术的局限性吗?干杯
  • 我通过在步骤 1 中反转 git log 生成命令的循环来调整此技术以避免该问题。即。与其对每个文件运行一次 git log,不如在命令行上使用文件列表只运行一次,并生成一个统一的日志。这样,修改 2 个或更多文件的提交在结果中仍然是一个提交,并且所有新提交都保持其原始的相对顺序。请注意,在(现在统一的)日志中重写文件名时,这也需要在步骤 2 中进行更改。我还使用 git am --committer-date-is-author-date 来保留原始提交时间戳。
  • 感谢您的实验和分享。我已经为其他读者更新了一些答案。但是,我花了一些时间来测试您的处理。如果您想提供命令行示例,请随时编辑此答案。干杯;)
【解决方案3】:

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

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

【讨论】:

  • 这应该被标记为正确答案。完全为我在同一个存储库中将文件从一个文件夹移动到另一个文件夹。我什至不必做“临时”的事情。 git mv olddir/file newdir/file 为我工作。
  • 所有历史记录都被保存了。
  • 为什么这比git mv oldfolder newfolder好?
【解决方案4】:

只需移动文件并使用:

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 Help Page

【讨论】:

    【解决方案5】:

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

    第 0 步:从 'master' 创建一个分支 'history' 以进行保管

    第一步:使用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
    

    第四步:连接本地丢失分支和源分支

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

    第 5 步:解决文件夹结构的合并冲突(如果出现提示)

    第 6 步:!!

    git push
    

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

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

    【讨论】:

    • 这在答案列表中应该更高,截至 2020 年 filter-repo 是此类操作的最佳选择。
    【解决方案6】:

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

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

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

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

    expressions.txt 是一个文件,其中包含 literal:OLD_NAME==&gt;NEW_NAME 之类的行(可以将 Python 的 RE 与 regex: 或 glob 与 glob: 一起使用)。

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

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

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

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

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

    【讨论】:

    • git: 'filter-repo' is not a git command. See 'git --help'
    • @alper 这个命令有效!但是 filter-repo 不是 Git 中的标准命令。您需要先安装它,然后才能使用它。你可以在这里找到如何下载和安装的说明github.com/newren/git-filter-repo
    【解决方案7】:

    虽然 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
    

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

     $ 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 版本的其他答案可能是正确的。

    【讨论】:

      【解决方案8】:

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

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

      【讨论】:

        【解决方案9】:

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

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

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

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

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

        【讨论】:

        • 我怀疑这是出于性能考虑。如果您不需要完整的历史记录,那么扫描内容肯定需要更长的时间。最简单的方法是设置一个别名git config alias.logf "log --follow",然后写git logf ./path/to/file
        • @TroelsThomsen this e-mail by Linus Torvalds,链接自 this answer,表明它是 Git 的有意设计选择,因为据称它比跟踪重命名等功能强大得多。
        • 这个答案有点误导。 Git 确实“检测重命名”,但在游戏中很晚;问题是问你如何确保 Git 跟踪重命名,阅读这篇文章的人可以很容易地推断出 Git 会自动为你检测并记录下来。它不是。 Git 没有真正处理重命名,而是有一些合并/日志工具试图找出发生了什么——而且很少能正确处理。 Linus 有一个错误但激烈的论点,为什么 git 永远不应该以正确的方式做事并明确地跟踪重命名。所以,我们被困在这里了。
        • 重要提示:如果重命名目录,例如在重命名 Java 包期间,请务必执行两次提交,第一次为 'git mv {old} {new}' 命令,第二次为更新引用更改的包目录的所有 Java 文件。否则即使使用 --follow 参数,git 也无法跟踪单个文件。
        • 虽然 Linus 可能犯的错误很少,但这似乎是一个。简单地重命名文件夹会导致大量增量上传到 GitHub。这让我对重命名文件夹持谨慎态度……但这对于程序员来说是一件相当大的直筒夹克。有时,我不得不重新定义事物的含义,或者改变事物的分类方式。莱纳斯:“换句话说,我是对的。我总是对的,但有时我比其他时候更正确。而且该死的,当我说‘文件无关紧要’时,我真的是对的( Tm值)。” ...我对此表示怀疑。
        【解决方案10】:

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

        /project/xyz
        

        /组件/xyz

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

        否(8 年后,Git 2.19,2018 年第三季度),因为 Git 会检测目录重命名,而且现在有更好的文档记录。

        参见commit b00bf1ccommit 1634688commit 0661e49commit 4d34dffcommit 983f464commit c840e1acommit 9929430(2018 年 6 月 27 日)和 commit d4e8062commit 5dacd4a(6 月 25 日) by Elijah Newren (newren).
        (由 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也想移动到@ 987654348@ by 提示整个目录“x”移动到“z”。

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

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

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

        一些基本规则限制何时 目录重命名检测适用:

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

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

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

        【讨论】:

          【解决方案11】:

          我愿意:

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

          【讨论】:

          • -u 似乎对我没有任何作用,是不是要更新历史记录?
          • 也许您想要-A 的行为?再次,请参见此处:git-scm.com/docs/git-add
          • 它会添加文件,但不会更新历史记录,因此“git log 文件名”会显示完整的历史记录。如果您仍然使用 --follow 选项,它只会显示完整的历史记录。
          • 我做了一个复杂的重构,移动了一个包含目录(使用 mv,而不是 git mv),然后在重命名的文件中更改了许多 #include 路径。 git 找不到足够的相似性来跟踪历史。但是 git add -u 正是我需要的东西。 git status 现在在显示“已删除”和“新文件”之前指示“重命名”。
          • 有很多关于 SO 的问题可以解决 git add -u 的目的。 Git 文档往往没有帮助,是我最不想看到的地方。这是一篇显示git add -u 的帖子:*.com/a/2117202
          【解决方案12】:

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

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

          现在,既然你还在我身边,你可能是一个单独的开发者,正在重命名一个完全孤立的文件。让我们使用filter-tree移动文件!

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

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

          改为:

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

          重做分支中的每个提交,在每次迭代的滴答声中执行命令。当你这样做时,很多事情都会出错。我通常会测试文件是否存在(否则它还没有移动),然后执行必要的步骤来根据我的喜好硬拔树。在这里,您可以通过文件 sed 更改对文件的引用等。把自己打昏! :)

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

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

          【讨论】:

          • 作为一个痴迷的 git-log-lover,我不会这样做。这些文件在那个时间点没有被命名,因此历史反映了一种从未存在过的情况。谁知道过去可能会发生什么测试!破坏早期版本的风险几乎在所有情况下都不值得。
          • @Vincent 你说得对,我试图尽可能清楚地说明这个解决方案不太可能是合适的。我也认为在这种情况下我们正在谈论“历史”这个词的两种含义,我很欣赏这两种含义。
          • 我发现有些情况下可能需要这个。假设我在自己的个人分支中开发了一些东西,现在我想将其合并到上游。但我发现,文件名不合适,所以我为我的整个个人分支更改了它。这样我就可以保持干净的正确历史记录,并从一开始就拥有正确的名称。
          • @user2291758 这是我的用例。这些更强大的 git 命令很危险,但这并不意味着如果你知道自己在做什么,它们就没有非常引人注目的用例!
          • @MattiJokipii:mvcommand 用于在每次提交之前在整个存储库的历史记录中移动文件,因此使用普通的 unix mv 是正确的。我什至不确定如果你使用git mv 会发生什么。如果您使用的是 Windows,则应使用 move 命令。
          【解决方案13】:
          git log --follow [file]
          

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

          【讨论】:

          • 这似乎要求您在开始修改文件之前只提交重命名。如果你移动文件(在 shell 中)然后改变它,所有的赌注都没有了。
          • @yoyo:那是因为 git 不跟踪重命名,它会检测到它们。 git mv 基本上是 git rm &amp;&amp; git add。有像 -M90 / --find-renames=90 这样的选项可以在文件 90% 相同时考虑对其进行重命名。
          最近更新 更多