我想重命名/移动Git中的项目子树
/project/xyz
to
/components/xyz
如果我使用普通的gitmv项目组件,那么xyz项目的所有提交历史都会丢失。有没有一种方法可以移动它,以保持历史?
我想重命名/移动Git中的项目子树
/project/xyz
to
/components/xyz
如果我使用普通的gitmv项目组件,那么xyz项目的所有提交历史都会丢失。有没有一种方法可以移动它,以保持历史?
Git检测重命名,而不是通过提交持久化操作,因此使用gitmv还是mv并不重要。
log命令采用--follow参数,该参数在重命名操作之前继续历史,即,它使用启发式搜索类似的内容。
要查找完整的历史记录,请使用以下命令:
git log --follow ./path/to/file
可以重命名文件并保持历史记录不变,尽管这会导致文件在存储库的整个历史记录中被重命名。这可能只适用于痴迷于git日志的爱好者,并有一些严重的影响,包括:
您可以重写共享历史,这是使用Git时最重要的“不要”。如果其他人克隆了存储库,您将通过此操作将其破坏。他们将不得不重新克隆以避免头痛。如果重命名足够重要,这可能是可以的,但您需要仔细考虑这一点——您可能会扰乱整个开源社区!如果您在存储库历史记录的早期使用文件的旧名称引用了该文件,那么实际上就是在破坏早期版本。为了解决这个问题,你必须多跳一点环跳。这不是不可能的,只是乏味而且可能不值得。
现在,既然你还和我在一起,你可能是一个单独的开发人员,正在重命名一个完全独立的文件。让我们使用过滤器树移动文件!
假设您要将一个旧文件移动到文件夹目录中,并将其命名为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目录才是必需的。if将避免在文件存在之前创建此文件夹。
No.
简短的答案是否定的。在Git中重命名文件并记住历史是不可能的。这是一种痛苦。
有传言说,git log--follow--查找副本会更加困难,但这对我来说不起作用,即使文件内容没有任何更改,而且这些移动都是用git mv完成的。
(最初,我使用Eclipse在一次操作中重命名和更新包,这可能会混淆Git。但这是一件非常常见的事情。--如果只执行mv,然后提交mv,并且mv不太远,那么follow似乎确实有效。)
Linus表示,您应该全面了解软件项目的全部内容,而不需要跟踪单个文件。不幸的是,我的小脑袋无法做到这一点。
这么多人无意识地重复了Git自动跟踪移动的说法,这真的很烦人。他们浪费了我的时间。Git没有这样做。根据设计(!)Git根本不跟踪移动。
我的解决方案是将文件重命名回其原始位置。更改软件以适合源代码管理。使用Git,您似乎只需要第一次“获取”它。
不幸的是,这打破了Eclipse,它似乎使用了--follow。gitlog--follow有时不会显示具有复杂重命名历史的文件的完整历史记录,尽管gitlog会显示。(我不知道为什么。)
(有一些过于聪明的黑客会重新开始旧的工作,但它们相当可怕。请参阅GitHub Gist:emiller/git mv with history。)
简而言之:如果Subversion这样做是错误的,那么Git这样做也是错误的——这样做不是什么(错误!)功能,这是错误的。
Yes
您可以使用git-log--prey=email将文件的提交历史转换为电子邮件补丁在新目录中重新组织这些文件并重命名它们您可以将这些文件(电子邮件)转换回Git提交,以使用gitam保存历史记录。
限制
未保留标记和分支路径文件重命名(目录重命名)时剪切历史记录
用示例逐步解释
1.以电子邮件格式提取历史记录
示例:提取文件3、文件4和文件5的历史记录
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 hard不能与--reverse组合使用。这就是为什么重命名文件(或重命名父目录)时会剪切历史记录。
电子邮件格式的临时历史记录:
/tmp/mail/dir
├── subdir
│ ├── file3
│ └── file4
└── file5
Dan Bonachea建议在第一步中反转git日志生成命令的循环:与其在每个文件中运行一次git日志,不如在命令行上使用文件列表运行一次,生成一个统一的日志。这样,修改多个文件的提交在结果中保持为一次提交,所有新的提交都保持其原始的相对顺序。注意,在重写(现在统一的)日志中的文件名时,这也需要在下面的第二步中进行更改。
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
--提交日期是作者日期,保留原始提交时间戳(Dan Bonachea的评论)。
您的其他回购现在是:
my_other_repo
├── dirF
│ ├── file55
│ └── file56
├── dirB
│ ├── dirB1
│ │ ├── file33
│ │ └── file44
│ └── dirB2
│ └── file5
└── dirH
└── file77
使用gitstatus查看准备推送的提交数量:-)
额外技巧:检查存储库中重命名/移动的文件
要列出已重命名的文件,请执行以下操作:
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
更多自定义:您可以使用选项--find copies hard或--reverse完成命令gitlog。还可以使用cut-f3-和grepping完整模式“{.*=>.*}”删除前两列。
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
虽然作为Git的核心,Git管道不会跟踪重命名,但如果你愿意,你用Git日志“瓷器”显示的历史可以检测它们。
对于给定的git日志,请使用-M选项:
git-log-p-M
使用当前版本的Git。
这也适用于其他命令,如gitdiff。
有一些选项可以使比较更严格或更不严格。如果您在重命名文件的同时不对文件进行重大更改,则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,这都是有效的,而不仅仅是使用gitlog。例如:
$ 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版本中是正确的。
我想重命名/移动Git中的项目子树/项目/xyz到/组件/xyz如果我使用普通的gitmv项目组件,那么xyz项目的所有提交历史都会丢失。
没有(8年后,Git 2.19,2018年第3季度),因为Git会检测到目录重命名,现在已经有了更好的记录。
参见Elijah Newren(Newren)提交的提交b00bf1c、提交1634688、提交0661e49、提交4d34dff、提交983f464、提交c840e1a、提交9929430(2018年6月27日)和提交d4e8062、提交5dacd4a(2018年7月25日)。(于2018年7月24日由Junio C Hamano(吉斯特)在提交时合并)
现在在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 filter-repo --path-rename OLD_NAME:NEW_NAME
要在提到目录的文件中重命名目录(可以使用回调,但我不知道如何):
git filter-repo --replace-text expressions.txt
expressions.txt是一个充满了文字: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以强制重写。(在执行此操作之前,您可能需要创建存储库的备份。)
如果不想保留引用(它们将显示在GitGUI的分支历史记录中),则必须添加--replace-refs delete no add。
我遵循这个多步骤过程将代码移动到父目录并保留历史记录。
步骤0:从“master”创建了分支“history”以进行安全保管
步骤1:使用gitfilter 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
注意:修改的历史记录和移动的文件夹似乎已提交。在此处输入代码
完成。代码移动到父目录/所需目录,保持历史记录完整!
只需移动文件并使用:
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 mv oldfolder temp && git mv temp newfolder
$ git commit
$ git push
在我的例子中,我将两个文件从“resources”目录移动到“src/main/resources”。如下代码所示,它们显示为“已删除”。
然而,在我将重新定位的文件添加到临时区域,然后添加删除的文件后,系统将它们识别为“重命名”。
当我检查这两个文件的历史记录时,它是完整的,它们的永久链接仍然有效。所以,一切都如我们所愿。
myaddress (master *)$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: resources/myaddress-schemas.sql
deleted: resources/select-sangdo-ro-60.sql
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
pom.xml
src/
myaddress (master *)$ git add src/main/resources/*.sql
myaddress (master *+)$ git add `git ls-files --deleted`
myaddress (master +)$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: resources/myaddress-schemas.sql -> src/main/resources/myaddress-schemas.sql
renamed: resources/select-sangdo-ro-60.sql -> src/main/resources/select-sangdo-ro-60.sql
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
pom.xml
src/main/java/
src/main/webapp/
myaddress (master +)$
myaddress (master +)$ git commit -m "two resource files moved to src/main"
[master 0832839] two resource files moved to src/main
2 files changed, 0 insertions(+), 0 deletions(-)
rename {resources => src/main/resources}/myaddress-schemas.sql (100%)
rename {resources => src/main/resources}/select-sangdo-ro-60.sql (100%)
myaddress (master)$