是否有可能在git中移动/重命名文件并保持其历史记录?

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

/project/xyz 

 /components/xyz 

如果我使用普通的git mv project componentsxyz project所有文件历史记录都将丢失。

有没有办法来移动这样的历史维护?

Git检测重命名而不是坚持提交的操作,所以无论你使用git mv或只是一个普通的mv并不重要。

但是,log命令会在重命名操作之前使用一个继续历史的--follow参数(也就是说,它使用启发式搜索相似的内容):

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

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

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

可以重命名文件并保持历史记录不变,尽管它会导致文件在存储库的整个历史记录中被重命名。 这可能只是为了迷恋git-log-lovers,并且有一些严重的含义,包括这些:

  • 你可以重写共享历史记录,这是使用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 

重做分支中的每个提交,在每个迭代的tick中执行命令。 当你这样做的时候,大量的东西可能会出错。 我通常会测试这个文件是否存在(否则它还没有移动),然后执行必要的步骤来根据我的喜好来调整树。 在这里你可能通过文件sed来改变文件的引用等等。 把自己打昏! 🙂

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

也; 当然,只有在将文件移动到新文件夹时,mkdir目录才是必需的。 如果将避免在历史早期创建此文件夹比您的文件存在。

没有。

简短的答案是NO ,不可能在Git中重命名文件并记住历史记录。 这是一个痛苦。

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

(最初我使用Eclipse在一个操作中重命名和更新软件包,这可能会混淆git,但是这是一个非常常见的事情 – 如果只执行一个mv ,然后commitmv不是太远。)

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

真的很烦人 ,很多人无意中重复了这个声明,git会自动跟踪移动。 他们浪费了我的时间。 Git没有这样的事情。 按设计(!)Git根本不跟踪移动。

我的解决方案是将文件重命名回原来的位置。 更改软件以适应源代码管理。 有了git,你似乎需要在第一时间正确使用它。

不幸的是,这打破了Eclipse,似乎使用 – --follow
git log --follow有时候不会显示具有复杂重命名历史的文件的完整历史记录,即使git log也行。 (我不知道为什么。)

(有一些太聪明的黑客回头重新承诺旧的工作,但他们相当可怕。请参阅GitHub-Gist: emiller / git-mv-with-history 。)

 git log --follow [file] 

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

我做:

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

目的

  • 使用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

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 '=>' 

更多定制:您可以使用选项--find-copies-harder --reverse--reverse来完成命令git log 。 您还可以使用cut -f3-cut -f3-完整模式“{。* =>。*}”删除前两列。

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

git的核心,git管道并没有跟踪重命名,你用git log“porcelain”显示的历史可以检测到它们,如果你喜欢的话。

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

git log -p -M

使用当前版本的git。

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

有多种选择可以使比较更为严格。 如果重命名文件而不对文件进行重大更改,则会使git日志和朋友更容易检测重命名。 出于这个原因,有些人在一次提交中重命名文件,并将其更改为另一次。

每当你要求git找到文件已经被重命名的地方,就要花费一些代价,所以不管你是否使用它,什么时候,取决于你。

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

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,这不仅仅适用于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 add -A 

其中放入的区域全部删除/新建文件。 这里git意识到文件被移动了。

 git commit -m "my message" git push 

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