是否有可能在git中移动/重命名文件并保持其历史记录?
我想重新命名/移动Git中的项目子树
/project/xyz
至
/components/xyz
如果我使用普通的git mv project components
则xyz 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
,然后commit
和mv
不是太远。)
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借来) - 添加复制/移动文件的提交历史记录
- 从一个目录到另一个目录
- 或从一个存储库到另一个存储库
局限性
- 标签和分支不被保留
- 历史是在路径文件重命名(目录重命名)
概要
- 使用电子邮件格式提取历史记录
git log --pretty=email -p --reverse --full-index --binary
- 重新组织文件树并更新文件名
- 使用追加新的历史记录
cat extracted-history | git am --committer-date-is-author-date
1.以电子邮件格式提取历史记录
示例:提取file3
, file4
和file5
历史记录
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
我不知道为什么,但这对我有用。