我想重命名/移动Git中的项目子树

/project/xyz

to

/components/xyz

如果我使用普通的gitmv项目组件,那么xyz项目的所有提交历史都会丢失。有没有一种方法可以移动它,以保持历史?


当前回答

我曾面临“重命名文件夹而不丢失历史记录”的问题。要修复此问题,请运行:

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

其他回答

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版本中是正确的。

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

步骤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 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。

在我的例子中,我将两个文件从“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)$