是否可以在Git中移动/重命名文件并保留其历史记录?

时间:2010-02-22 22:11:07

标签: git rename mv

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

/project/xyz

/components/xyz

如果我使用普通git mv project components,则xyz project的所有提交历史记录都会丢失。有没有办法移动这个以保持历史?

14 个答案:

答案 0 :(得分:599)

Git检测重命名而不是使用提交持久保存操作,因此无论使用git mv还是mv都无关紧要。

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

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

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

git log --follow ./path/to/file

答案 1 :(得分:88)

可能重命名文件并保持历史记录不变,尽管它会导致文件在整个存储库历史记录中重命名。这可能只适用于痴迷的git-log爱好者,并且有一些严重的影响,包括:

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

现在,既然你还在和我在一起,那么你可能是个人开发人员重命名一个完全孤立的文件。我们使用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

重做分支中的每个提交,在每次迭代的ticks中执行命令。当你这样做时,很多东西都可能出错。我通常会测试文件是否存在(否则它还没有移动)然后执行必要的步骤来按照自己的喜好对树进行修剪。在这里,您可以通过文件来修改对文件的引用,等等。把自己打昏! :)

完成后,文件将被移动并且日志完好无损。你觉得自己像个忍者海盗。

也;仅当您将文件移动到新文件夹时,才需要mkdir目录。 if 将避免在历史记录中早于文件存在而创建此文件夹。

答案 2 :(得分:76)

没有

简短的回答是 NO ,无法在Git中重命名文件并记住历史记录。这是一种痛苦。

谣言认为git log --follow --find-copies-harder会起作用,但它对我不起作用,即使文件内容没有任何变化,并且已经使用git mv进行了移动

(最初我使用Eclipse在一个操作中重命名和更新包,这可能让git感到困惑。但这是一件非常常见的事情。--follow似乎只有mv才有效已执行,然后commitmv不太远。)

Linus说,您应该全面了解软件项目的全部内容,而不需要跟踪单个文件。好吧,可悲的是,我的小脑子不能这样做。

非常烦人,很多人盲目地重复了git自动跟踪动作的说法。他们浪费了我的时间。 Git没有这样的事情。 By design(!) Git does not track moves at all.

我的解决方案是将文件重命名回原始位置。更改软件以适合源控件。使用git,你似乎需要第一次正确使用它。

不幸的是,这破坏了Eclipse,它似乎使用了--follow   git log --follow 尽管有时也没有显示具有复杂重命名历史的文件的完整历史记录   git log 确实。 (我不知道为什么。)

(有一些太聪明的黑客可以追溯并重新投入旧作,但它们相当令人恐惧。请参阅GitHub-Gist:emiller/git-mv-with-history。)

答案 3 :(得分:40)

git log --follow [file]

将通过重命名向您显示历史记录。

答案 4 :(得分:17)

我做:

git mv {old} {new}
git add -u {new}

答案 5 :(得分:14)

目标

  • 使用git am (灵感来自Smar,借鉴Exherbo
  • 添加复制/移动文件的提交历史记录
  • 从一个目录到另一个目录
  • 或从一个存储库到另一个存储库

限制

  • 不保留标签和分支
  • 历史记录在路径文件重命名(目录重命名)
  • 上被删除

摘要

  1. 使用
    以电子邮件格式提取历史记录 git log --pretty=email -p --reverse --full-index --binary
  2. 重新组织文件树并更新文件名
  3. 使用
    附加新历史记录 cat extracted-history | git am --committer-date-is-author-date
  4. 1。以电子邮件格式提取历史记录

    示例:摘录file3file4file5

    的历史记录
    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-harder无法与--reverse结合使用。这就是重命名文件时(或重命名父目录时)切断历史记录的原因。

    电子邮件格式的临时历史记录:

    /tmp/mail/dir
        ├── subdir
        │   ├── file3
        │   └── file4
        └── file5
    

    Dan Bonachea建议在第一步中反转git log generation命令的循环:不是每个文件运行一次git log,而是在命令行上使用一个文件列表运行一次并生成一个统一日志。这种方式提交修改多个文件仍然是结果中的单个提交,并且所有新提交都保持其原始相对顺序。请注意,在(现在统一)日志中重写文件名时,还需要在下面的第二步中进行更改。

    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
    

    --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--find-copies-harder完成命令--reverse。您还可以使用cut -f3-删除前两列并点击完整模式' {。* => *}'

    find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
    

答案 6 :(得分:13)

  

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

/project/xyz
     

     

/ components / xyz

     

如果我使用普通的git mv project components,则xyz项目的所有提交历史都将丢失。

否(8年后,Git 2.19,2018年第三季度),因为Git将检测目录重命名,并且现在对此进行了更好的记录。

请参见commit b00bf1ccommit 1634688commit 0661e49commit 4d34dffcommit 983f464commit c840e1acommit 9929430(2018年6月27日) ,以及commit d4e8062commit 5dacd4aElijah Newren (newren)(2018年6月25日)。
(由Junio C Hamano -- gitster --commit 0ce5a69中合并,2018年7月24日)

现在在Documentation/technical/directory-rename-detection.txt中对此进行了解释:

示例:

  

x/ax/bx/c全部移到z/az/bz/c时,{{同时添加的1}}也希望移至x/d   暗示整个目录'z/d'已移至'x'。

但是还有很多其他情况,例如:

  

历史记录的一侧重命名为z,另一侧将某些文件重命名为   x -> z,导致合并需要进行传递重命名。

为简化目录重命名检测,这些规则由Git强制执行:

一些基本规则限制 目录重命名检测适用:

  
      
  1. 如果给定目录仍然存在于合并的两侧,则我们不认为该目录已被重命名。
  2.   
  3. 如果要重命名的文件子集具有(或可能互相干扰)文件或目录,请为这些特定子路径“关闭”目录重命名并报告冲突给用户。
  4.   
  5. 如果历史记录的另一侧确实将目录重命名为您的历史记录的另一侧重命名的路径,那么对于任何隐式目录重命名,请从历史记录的另一侧忽略该特定的重命名(但警告用户)。
  6.   

您可以在t/t6043-merge-rename-directories.sh中看到很多测试 ,这些测试还指出:

  
      
  • a)如果重命名将目录分为两个或多个其他目录,则重命名最多的目录是“ wins”。
  •   
  • b)避免对目录进行目录重命名检测,如果该路径是合并两侧的重命名源。
  •   
  • c)仅在另一侧将隐式目录重命名应用于目录   历史就是重命名了。
  •   

答案 7 :(得分:6)

我遵循了此多步骤过程,将代码移至父目录并保留了历史记录。

第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

完成。代码移至父目录/所需目录,保持历史记录完整!

答案 8 :(得分:6)

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

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

答案 9 :(得分:4)

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

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与regex:一起使用,或者将glob与glob:一起使用)。

要在提交消息中重命名目录,请执行以下操作:

git-filter-repo --message-callback 'return message.replace(b"OLD_NAME", b"NEW_NAME")'

还支持Python的正则表达式,但必须手动用Python编写。

如果存储库是原始存储库,没有远程存储库,则必须添加--force才能强制重写。 (在执行此操作之前,您可能需要创建存储库的备份。)

如果不想保留引用(它们将显示在Git GUI的分支历史记录中),则必须添加--replace-refs delete-no-add

答案 10 :(得分:2)

虽然是git的核心,git管道不会跟踪重命名,但是如果你愿意的话,使用git log“porcelain”显示的历史可以检测到它们。

对于给定的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

请注意,无论何时使用差异,这都有效,而不仅仅是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

作为试验,我对功能分支中的一个文件进行了一些小改动并提交了它,然后在主分支中重命名了文件,提交了,然后对文件的另一部分进行了一些小改动并提交了。当我转到功能分支并从主服务器合并时,合并重命名了该文件并合并了更改。这是合并的输出:

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

答案 11 :(得分:1)

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

然后,对最终提交到单独提交中的文件内容进行任何最终更改。

答案 12 :(得分:1)

只需移动文件并进行以下操作:

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 Help Page中提取。

答案 13 :(得分:-2)

我移动文件然后执行

git add -A

将所有已删除/新文件放入sataging区域。这里git意识到文件被移动了。

git commit -m "my message"
git push

我不知道为什么但这对我有用。