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

/project/xyz

to

/components/xyz

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


当前回答

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 '{.* => .*}'

其他回答

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

要重命名目录或文件(我不太了解复杂的情况,因此可能需要注意):

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。

我愿意:

git mv {old} {new}
git add -u {new}
git log --follow [file]

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

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这样做也是错误的——这样做不是什么(错误!)功能,这是错误的。