764

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

/project/xyz

/components/xyz

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

4

13 回答 13

702

Git 会检测重命名,而不是通过提交来持久化操作,因此无论您使用git mv与否mv都无关紧要。

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

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

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

git log --follow ./path/to/file
于 2010-02-22T22:26:14.917 回答
154

不。

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

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

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

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

这么多人不自觉地重复了 Git 自动跟踪移动的说法,这真的很烦人。他们浪费了我的时间。Git 不做这样的事情。按照设计(!)Git 根本不跟踪移动。

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

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

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

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

于 2015-09-28T05:46:07.587 回答
113

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

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

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

假设您要将文件移动到文件olddir中并为其命名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

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

完成后,文件被移动并且日志是完整的。你感觉自己像个忍者海盗。

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

于 2013-02-28T11:59:42.850 回答
46
git log --follow [file]

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

于 2010-02-22T22:25:05.790 回答
24

我愿意:

git mv {old} {new}
git add -u {new}
于 2012-11-24T19:07:27.490 回答
21

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

/project/xyz

/组件/xyz

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

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

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

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

例子:

当所有x/a,x/bx/c都移动到z/a,z/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) 仅当历史的另一侧是进行重命名的一方时,才将隐式目录重命名应用于目录。
于 2018-07-30T19:37:40.690 回答
18

是的

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

局限性

  • 不保留标签和分支
  • 路径文件重命名(目录重命名)时切入历史

用例子一步一步解释

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

示例:提取和file3的历史记录file4file5

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"' {} ';'

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


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

要列出已重命名的文件:

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

更多自定义:您可以git log使用 options--find-copies-harder--reverse. 您还可以使用cut -f3-和 grepping complete pattern删除前两列'{.* => .*}'

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
于 2015-11-24T18:36:25.677 回答
13

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

$ git mv oldfolder temp && git mv temp newfolder
$ git commit
$ git push
于 2020-06-23T09:07:41.507 回答
9

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

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 withregex:或 glob with 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.

于 2020-03-03T16:10:21.240 回答
9

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

第 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

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

于 2020-04-19T01:51:37.823 回答
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 帮助页面

于 2020-04-19T14:45:19.057 回答
3

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

于 2017-02-23T04:54:04.083 回答
1

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

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

于 2020-02-06T06:08:18.473 回答