我想在 Git 中重命名/移动项目子树,将其从
/project/xyz
到
/components/xyz
如果我使用 plain git mv project components
,那么所有的提交历史xyz project
都会丢失。有没有办法移动它以保持历史?
Git 会检测重命名,而不是通过提交来持久化操作,因此无论您使用git mv
与否mv
都无关紧要。
该log
命令采用--follow
在重命名操作之前继续历史记录的参数,即,它使用启发式搜索相似的内容:
http://git-scm.com/docs/git-log
要查找完整的历史记录,请使用以下命令:
git log --follow ./path/to/file
简短的回答是否定的。在 Git 中重命名文件并记住历史是不可能的。这是一种痛苦。
有传言说它git log --follow
--find-copies-harder
会起作用,但它对我不起作用,即使文件内容的更改为零,并且已经使用git mv
.
(最初我使用 Eclipse 在一个操作中重命名和更新包,这可能会使 Git 感到困惑。但这是一件很常见的事情。如果只执行 a 然后 a并且不太远--follow
,似乎确实有效。)mv
commit
mv
Linus 说你应该全面了解软件项目的全部内容,而不需要跟踪单个文件。好吧,可悲的是,我的小脑袋无法做到这一点。
这么多人不自觉地重复了 Git 自动跟踪移动的说法,这真的很烦人。他们浪费了我的时间。Git 不做这样的事情。按照设计(!)Git 根本不跟踪移动。
我的解决方案是将文件重命名回其原始位置。更改软件以适应源代码管理。使用 Git,您似乎只需要第一次就正确地“git”它。
不幸的是,这破坏了似乎使用--follow
. git log --follow
有时即使显示,也不会显示具有复杂重命名历史的文件的完整历史git log
。(我不知道为什么。)
(有一些过于聪明的 hack 可以返回并重新提交旧工作,但它们相当可怕。请参阅 GitHub-Gist:emiller/git-mv-with-history。)
简而言之:如果Subversion 这样做是错误的,那么 Git 这样做也是错误的 - 这样做不是一些(错误!)功能,而是一个错误。
重命名文件并保持历史完整是可能的,尽管它会导致文件在整个存储库历史中被重命名。这可能仅适用于痴迷的 git-log-lovers,并且有一些严重的影响,包括:
现在,既然你还在我身边,你可能是一个单独的开发者,正在重命名一个完全隔离的文件。让我们使用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
将重做分支中的每个提交,在每次迭代的滴答声中执行命令。当你这样做时,很多事情都会出错。我通常会测试文件是否存在(否则它还没有移动),然后执行必要的步骤来根据我的喜好硬塞树。在这里,您可以通过文件 sed 更改对文件的引用等。把自己打昏!:)
完成后,文件被移动并且日志是完整的。你感觉自己像个忍者海盗。
还; 当然,只有将文件移动到新文件夹时,才需要 mkdir 目录。if将避免在您的文件存在之前在历史记录中创建此文件夹。
git log --follow [file]
将通过重命名向您展示历史。
我愿意:
git mv {old} {new}
git add -u {new}
我想在 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/b
和x/c
都移动到z/a
,z/b
和z/c
时,很可能x/d
同时添加的也想z/d
通过提示整个目录 'x
' 移动到 'z
' 来移动。
但它们还有许多其他情况,例如:
一侧历史重命名
x -> z
,另一侧将某些文件重命名为x/e
,导致合并需要进行传递重命名。
为了简化目录重命名检测,Git 强制执行这些规则:
应用目录重命名检测时有几个基本规则限制:
- 如果给定目录仍然存在于合并的两侧,我们不认为它已被重命名。
- 如果要重命名的文件子集有文件或目录妨碍(或相互妨碍),则“关闭”那些特定子路径的目录重命名并向用户报告冲突.
- 如果历史的另一方将目录重命名为您的历史重命名的路径,则忽略历史另一侧的特定重命名以进行任何隐式目录重命名(但警告用户)。
您可以在 中看到很多测试t/t6043-merge-rename-directories.sh
,其中还指出:
- a) 如果重命名将一个目录拆分为两个或多个其他目录,则重命名最多的目录“获胜”。
- b) 如果该路径是合并任一侧的重命名源,则避免对路径进行目录重命名检测。
- c) 仅当历史的另一侧是进行重命名的一方时,才将隐式目录重命名应用于目录。
git log --pretty=email
git am
.示例:提取和file3
的历史记录file4
file5
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 --follow
or--find-copies-harder
不能与--reverse
. 这就是为什么当文件被重命名(或父目录被重命名)时历史被删除的原因。
电子邮件格式的临时历史记录:
/tmp/mail/dir
├── subdir
│ ├── file3
│ └── file4
└── file5
Dan Bonachea建议在第一步中反转 git log 生成命令的循环:与其对每个文件运行一次 git log,不如在命令行上使用文件列表只运行一次并生成单个统一日志。这样,修改多个文件的提交在结果中仍然是一个提交,并且所有新提交都保持其原始的相对顺序。请注意,在(现在统一的)日志中重写文件名时,这也需要在下面的第二步中进行更改。
假设您想将这三个文件移动到另一个 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"' {} ';'
您的另一个回购是:
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 '{.* => .*}'
我遇到了“重命名文件夹而不丢失历史记录”的问题。要修复它,请运行:
$ git mv oldfolder temp && git mv temp newfolder
$ git commit
$ git push
重命名目录或文件(我不太了解复杂的情况,所以可能会有一些警告):
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
.
我按照这个多步骤过程将代码移动到父目录并保留历史记录。
第 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
完毕。代码移动到父目录/所需目录,保持历史不变!
只需移动文件和阶段:
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 的核心,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 版本的其他答案可能是正确的。
首先创建一个仅重命名的独立提交。
然后对文件内容的任何最终更改都放在单独的提交中。