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

Is it possible to move/rename files in Git and maintain their history?

我想在Git中重命名/移动项目子树,将其从

1
/project/xyz

1
/components/xyz

如果我使用一个普通的git mv project components,那么xyz project的所有提交历史都会丢失。有没有办法让历史得以延续?


Git检测重命名,而不是使用commit来持久化操作,因此使用git mvmv并不重要。

log命令接受一个--follow参数,该参数在重命名操作之前继续历史记录,即使用启发式搜索类似内容:

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

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

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


可以重命名一个文件并保持历史完整,尽管它会导致在存储库的整个历史中重命名该文件。这可能只适用于痴迷于Git日志的爱好者,并有一些严重的影响,包括:

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

现在,既然你还和我在一起,你可能是一个单独的开发者,正在重命名一个完全独立的文件。让我们用filter-tree移动一个文件!

假设您要将一个文件old移动到dir文件夹中,并将其命名为new

这可以用git mv old dir/new && git add -u dir/new完成,但这打破了历史。

相反:

1
git filter-branch --tree-filter 'if [ -f old ]; then mkdir dir && mv old dir/new; fi' HEAD

将重做分支中的每个提交,在每次迭代的标记中执行命令。当你这样做的时候,很多事情都会出错。我通常会测试文件是否存在(否则它还没有移动),然后执行必要的步骤按我的喜好把树推到合适的位置。在这里,您可以通过文件来更改对文件的引用,等等。振作起来!:)

完成后,文件将被移动,日志将保持完整。你觉得自己像个忍者海盗。

当然,只有将文件移到新文件夹中时,mkdir目录才是必需的。if将避免在历史记录中早于文件存在的时间创建此文件夹。


不。

简短的回答是"否",不可能在git中重命名文件并记住历史。这是一种痛苦。

有传言说,git log --follow--find-copies-harder会起作用,但对我来说不起作用,即使文件内容没有任何变化,而且这些移动都是用git mv做的。

(最初我使用Eclipse在一个操作中重命名和更新包,这可能会混淆Git。但这是一件很常见的事情。如果只执行mv,然后执行commitmv不太远,--follow似乎确实有效。)

Linus说,应该从整体上理解软件项目的全部内容,而不需要跟踪单个文件。可悲的是,我的小脑袋不能做到这一点。

这么多人无心重复了Git自动跟踪移动的声明,真的很烦人。他们浪费了我的时间。吉特不做这种事。按设计!!)Git根本不跟踪移动。

我的解决方案是将文件重命名回其原始位置。更改软件以适合源代码管理。有了Git,你似乎第一次就需要正确的Git。

不幸的是,这打破了Eclipse,后者似乎使用了--follow。埃多克斯1〔9〕有时不显示具有复杂重命名历史的文件的完整历史记录,即使埃多克斯1〔18〕做。(我不知道为什么。)

(有一些太聪明的黑客回去重新投入旧的工作,但他们相当可怕。参见Github要旨:Emiller/Git MV和历史。)


1
git log --follow [file]

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


我愿意:

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


目标

  • use git am(inspired from smar,borrowed from exherbo)
  • 添加复制/移动文件的提交历史记录
  • 从一个目录到另一个目录
  • 或者从一个存储库到另一个存储库

限制

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

总结

  • 用电子邮件格式提取历史记录江户十一〔一〕号
  • 重新组织文件树并更新文件名
  • 使用附加新历史记录埃多克斯1〔2〕
  • 1。以电子邮件格式提取历史记录

    示例:提取file3file4file5的历史记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    my_repo
    ├── dirA
    │   ├── file1
    │   └── file2
    ├── dirB            ^
    │   ├── subdir      | To be moved
    │   │   ├── file3   | with history
    │   │   └── file4   |
    │   └── file5       v
    └── dirC
        ├── file6
        └── file7

    设置/清理目的地

    1
    2
    export historydir=/tmp/mail/dir       # Absolute path
    rm -rf"$historydir"    # Caution when cleaning the folder

    以电子邮件格式提取每个文件的历史记录

    1
    2
    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组合。这就是重命名文件(或重命名父目录)时剪切历史的原因。

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

    1
    2
    3
    4
    5
    /tmp/mail/dir
        ├── subdir
        │   ├── file3
        │   └── file4
        └── file5

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

    2。重新组织文件树并更新文件名

    假设您想在另一个repo中移动这三个文件(可以是同一个repo)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

    因此,重新组织文件:

    1
    2
    3
    4
    5
    6
    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

    您的临时历史记录现在是:

    1
    2
    3
    4
    5
    6
    7
    /tmp/mail/dir
        └── dirB
            ├── dirB1
            │   ├── file33
            │   └── file44
            └── dirB2
                 └── file5

    同时更改历史记录中的文件名:

    1
    2
    cd"$historydir"
    find * -type f -exec bash -c 'sed"/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i"$0"' {} ';'

    三。应用新历史记录

    你的其他回购协议是:

    1
    2
    3
    4
    5
    6
    my_other_repo
    ├── dirF
    │   ├── file55
    │   └── file56
    └── dirH
        └── file77

    从临时历史文件应用提交:

    1
    2
    cd my_other_repo
    find"$historydir" -type f -exec cat {} + | git am --committer-date-is-author-date

    --committer-date-is-author-date保留了最初的提交时间戳(dan bonacha的评论)。

    您的其他回购协议现在是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    my_other_repo
    ├── dirF
    │   ├── file55
    │   └── file56
    ├── dirB
    │   ├── dirB1
    │   │   ├── file33
    │   │   └── file44
    │   └── dirB2
    │        └── file5
    └── dirH
        └── file77

    使用git status查看准备推送的提交量:—)

    额外技巧:检查repo中重命名/移动的文件

    要列出已重命名的文件:

    1
    find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'

    更多定制:您可以使用选项--find-copies-harder--reverse完成命令git log。您还可以使用cut -f3-和grepping complete pattern'.*=>.*删除前两列。

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


    I would like to rename/move a project subtree in Git moving it from

    1
    /project/xyz

    to

    /components/xyz

    If I use a plain git mv project components, then all the commit history for the xyz project gets lost.

    没有(8年后,Git2.19,2018年第3季度),因为Git将检测到目录重命名,现在这是更好的记录。

    见Elijah Newren(newren提交的commit b00bf1c、commit 1634688、commit 0661e49、commit 4d34dff、commit 983f464、commit c840e1a、commit 9929430(2018年6月27日)和commit d4e8062、commit 5dac4a(2018年6月25日)。(由Junio C Hamano--gitster于2018年7月24日在Commit 0ce5a69中合并)

    这在Documentation/technical/directory-rename-detection.txt中解释:

    例子:

    When all of x/a, x/b and x/c have moved to z/a, z/b and z/c, it is likely that x/d added in the meantime would also want to move to z/d by
    taking the hint that the entire directory 'x' moved to 'z'.

    但也有很多其他情况,比如:

    one side of history renames x -> z, and the other renames some file to
    x/e, causing the need for the merge to do a transitive rename.

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

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

  • If a given directory still exists on both sides of a merge, we do not consider it to have been renamed.
  • If a subset of to-be-renamed files have a file or directory in the way (or would be in the way of each other),"turn off" the directory rename for those specific sub-paths and report the conflict to the user.
  • If the other side of history did a directory rename to a path that your side of history renamed away, then ignore that particular rename from the other side of history for any implicit directory renames (but warn the user).
  • 您可以在t/t6043-merge-rename-directories.sh中看到许多测试,它们还指出:

    • a) If renames split a directory into two or more others, the directory with the most renames,"wins".
    • b) Avoid directory-rename-detection for a path, if that path is the source of a rename on either side of a merge.
    • c) Only apply implicit directory renames to directories if the other side
      of history is the one doing the renaming.


    虽然Git的核心是Git管道,但它不跟踪重命名,但如果您愿意,可以通过Git日志"瓷质"来检测它们。

    对于给定的git log,使用-m选项:

    git log -p -M

    使用当前版本的Git。

    这也适用于其他命令,如git diff

    有一些选项可以使比较更严格或更不严格。如果重命名文件时不同时对文件进行重大更改,则Git日志和朋友更容易检测到重命名。因此,有些人在一次提交中重命名文件,在另一次提交中更改文件。

    每当你要求Git查找文件的重命名位置时,CPU的使用都会有一定的代价,所以无论你是否使用它,以及何时使用,都取决于你自己。

    如果希望在特定存储库中始终报告具有重命名检测功能的历史记录,则可以使用:

    git config diff.renames 1

    检测到从一个目录移动到另一个目录的文件。下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    commit c3ee8dfb01e357eba1ab18003be1490a46325992
    Author: John S. Gruber <[email protected]>
    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 <[email protected]>
    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一起使用。例如:

    1
    2
    3
    4
    5
    $ 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合并时,合并重命名了文件并合并了更改。下面是合并的输出:

    1
    2
    3
    4
    5
    6
     $ 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版本是正确的。


    我移动文件,然后做

    1
    git add -A

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

    1
    2
    git commit -m"my message"
    git push

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