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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2314652/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-10 07:54:19  来源:igfitidea点击:

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

gitrenamemv

提问by sgargan

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

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

/project/xyz

to

/components/xyz

If I use a plain git mv project components, then all the commit history for the xyz projectgets lost. Is there a way to move this such that the history is maintained?

如果我使用 plain git mv project components,那么所有提交历史记录xyz project都会丢失。有没有办法移动它以保持历史?

采纳答案by Troels Thomsen

Git detects renames rather than persisting the operation with the commit, so whether you use git mvor mvdoesn't matter.

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

The logcommand takes a --followargument that continues history before a rename operation, i.e., it searches for similar content using the heuristics:

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

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

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

To lookup the full history, use the following command:

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

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

回答by ?ystein Steimler

It is possibleto rename a file and keep the history intact, although it causes the file to be renamed throughout the entire history of the repository. This is probably only for the obsessive git-log-lovers, and has some serious implications, including these:

可能重命名文件,并保持完好的历史,但它会导致整个仓库的整个历史要重命名的文件。这可能仅适用于痴迷的 git-log-lovers,并且有一些严重的影响,包括:

  • You could be rewriting a shared history, which is the most important DON'T while using Git. If someone else has cloned the repository, you'll break it doing this. They will have to re-clone to avoid headaches. This might be OK if the rename is important enough, but you'll need to consider this carefully -- you might end up upsetting an entire opensource community!
  • If you've referenced the file using it's old name earlier in the repository history, you're effectively breaking earlier versions. To remedy this, you'll have to do a bit more hoop jumping. It's not impossible, just tedious and possibly not worth it.
  • 您可能正在重写共享历史记录,这是使用 Git 时最重要的“不要”。如果其他人已经克隆了存储库,您将通过这样做来破坏它。他们将不得不重新克隆以避免头痛。如果重命名足够重要,这可能没问题,但是您需要仔细考虑这一点 - 您最终可能会扰乱整个开源社区!
  • 如果您在存储库历史记录中较早使用旧名称引用了该文件,那么您实际上是在破坏早期版本。为了解决这个问题,你必须多做一些箍跳。这并非不可能,只是乏味且可能不值得。

Now, since you're still with me, you're a probably solo developer renaming a completely isolated file. Let's move a file using filter-tree!

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

Assume you're going to move a file oldinto a folder dirand give it the name new

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

This could be done with git mv old dir/new && git add -u dir/new, but that breaks history.

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

Instead:

反而:

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

will redoevery commit in the branch, executing the command in the ticks for each iteration. Plenty of stuff can go wrong when you do this. I normally test to see if the file is present (otherwise it's not there yet to move) and then perform the necessary steps to shoehorn the tree to my liking. Here you might sed through files to alter references to the file and so on. Knock yourself out! :)

重做分支中的每个提交,在每次迭代的刻度中执行命令。当你这样做时,很多事情都会出错。我通常会测试该文件是否存在(否则它尚未移动),然后执行必要的步骤以根据我的喜好硬拔树。在这里,您可以通过文件 sed 来更改对文件的引用等。把自己打昏!:)

When completed, the file is moved and the log is intact. You feel like a ninja pirate.

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

Also; The mkdir dir is only necessary if you move the file to a new folder, of course. The ifwill avoid the creation of this folder earlier in history than your file exists.

还; 当然,只有将文件移动到新文件夹时才需要 mkdir 目录。该如果将避免在历史上较早的该文件夹的创建比你的文件存在。

回答by Tuntable

No.

不。

The short answer is NO. It is not possible to rename a file in Git and remember the history. And it is a pain.

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

Rumor has it that git log --follow--find-copies-harderwill work, but it does not work for me, even if there are zero changes to the file contents, and the moves have been made with git mv.

有传言说它git log --follow--find-copies-harder会起作用,但它对我不起作用,即使文件内容更改为零,并且已使用git mv.

(Initially I used Eclipse to rename and update packages in one operation, which may have confused Git. But that is a very common thing to do. --followdoes seem to work if only a mvis performed and then a commitand the mvis not too far.)

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

Linus says that you are supposed to understand the entire contents of a software project holistically, not needing to track individual files. Well, sadly, my small brain cannot do that.

Linus 说你应该全面了解软件项目的全部内容,而不需要跟踪单个文件。好吧,可悲的是,我的小脑袋无法做到这一点。

It is really annoyingthat so many people have mindlessly repeated the statement that Git automatically tracks moves. They have wasted my time. Git does no such thing. By design(!) Git does not track moves at all.

这是真的很烦,所以很多人都盲目地重复了Git自动追踪移动的声明。他们浪费了我的时间。Git 不做这样的事情。按照设计(!)Git 根本不跟踪移动。

My solution is to rename the files back to their original locations. Change the software to fit the source control. With Git you just seem to need to "git" it right the first time.

我的解决方案是将文件重命名回其原始位置。更改软件以适应源代码管理。使用 Git,您似乎只需要第一次就正确地“git”。

Unfortunately, that breaks Eclipse, which seems to use --follow. git log --followsometimes does not show the full history of files with complicated rename histories even though git logdoes. (I do not know why.)

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

(There are some too clever hacks that go back and recommit old work, but they are rather frightening. See GitHub-Gist: emiller/git-mv-with-history.)

(有一些太聪明的 hack 会返回并重新提交旧工作,但它们相当可怕。请参阅 GitHub-Gist:emiller/git-mv-with-history。)

回答by Erik Hesselink

git log --follow [file]

will show you the history through renames.

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

回答by James M. Greene

I do:

我愿意:

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

回答by VonC

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

/project/xyz

to

/components/xyz

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

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

/project/xyz

/组件/xyz

如果我使用一个普通的git mv project components,那么项目的所有提交历史xyz都会丢失。

No (8 years later, Git 2.19, Q3 2018), because Git will detect the directory rename, and this is now better documented.

否(8 年后,Git 2.19,2018 年第三季度),因为 Git 会检测目录 rename,现在有更好的文档记录。

See commit b00bf1c, commit 1634688, commit 0661e49, commit 4d34dff, commit 983f464, commit c840e1a, commit 9929430(27 Jun 2018), and commit d4e8062, commit 5dacd4a(25 Jun 2018) by Elijah Newren (newren).
(Merged by Junio C Hamano -- gitster--in commit 0ce5a69, 24 Jul 2018)

提交b00bf1c提交1634688提交0661e49提交4d34dff提交983f464提交c840e1a提交9929430(2018年6月27日),以及提交d4e8062提交5dacd4a由(2018年6月25日)以利亚Newren( )newren
(由Junio C gitsterHamano合并-- --commit 0ce5a69,2018 年 7 月 24 日)

That is now explained in Documentation/technical/directory-rename-detection.txt:

现在解释Documentation/technical/directory-rename-detection.txt如下:

Example:

例子:

When all of x/a, x/band x/chave moved to z/a, z/band z/c, it is likely that x/dadded in the meantime would also want to move to z/dby taking the hint that the entire directory 'x' moved to 'z'.

当 all of x/a, x/bandx/c都移到z/a, z/band 时z/c,很可能x/d同时添加的也希望z/d通过提示整个目录“ x”移动到“ z”来移动到。

But they are many other cases, like:

但它们还有许多其他情况,例如:

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.

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

To simplify directory rename detection, those rules are enforced by Git:

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

a couple basic rules limit when directory rename detection applies:

目录重命名检测适用时的几个基本规则限制:

  1. If a given directory still exists on both sides of a merge, we do not consider it to have been renamed.
  2. 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.
  3. 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).
  1. 如果给定目录在合并的两侧仍然存在,我们不认为它已被重命名。
  2. 如果要重命名的文件的子集有文件或目录妨碍(或将相互妨碍),“关闭”这些特定子路径的目录重命名并向用户报告冲突.
  3. 如果历史的另一侧将目录重命名为您的历史侧重命名的路径,则忽略来自历史另一侧的特定重命名以进行任何隐式目录重命名(但警告用户)。

You can see a lotof tests in t/t6043-merge-rename-directories.sh, which also point out that:

你可以在 中看到很多测试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.
  • a) 如果重命名将目录拆分为两个或多个其他目录,重命名次数最多的目录“获胜”。
  • b) 避免对路径进行目录重命名检测,如果该路径是合并任一侧的重命名源。
  • c) 仅当历史的另一侧是进行重命名的人时,才对目录应用隐式目录重命名。

回答by olibre

Objective

客观的

  • Use git am(inspired from Smar, borrowed from Exherbo)
  • Add commit history of copied/moved files
  • From one directory to another
  • Or from one repository to another
  • 使用(灵感来自Smar,借自Exherbogit am
  • 添加复制/移动文件的提交历史
  • 从一个目录到另一个目录
  • 或者从一个存储库到另一个

Limitation

局限性

  • Tags and branches are not kept
  • History is cut on path file rename (directory rename)
  • 不保留标签和分支
  • 历史在路径文件重命名(目录重命名)时被剪切

Summary

概括

  1. Extract history in email format using
    git log --pretty=email -p --reverse --full-index --binary
  2. Reorganize file tree and update filenames
  3. Append new history using
    cat extracted-history | git am --committer-date-is-author-date
  1. 使用电子邮件格式提取历史记录
    git log --pretty=email -p --reverse --full-index --binary
  2. 重新组织文件树并更新文件名
  3. 使用附加新历史记录
    cat extracted-history | git am --committer-date-is-author-date


1. Extract history in email format

1.以电子邮件格式提取历史记录

Example: Extract history of file3, file4and file5

示例:提取历史file3file4file5

my_repo
├── dirA
│?? ├── file1
│?? └── file2
├── dirB            ^
│   ├── subdir      | To be moved
│   │   ├── file3   | with history
│   │   └── file4   | 
│?? └── file5       v
└── dirC
    ├── file6
    └── file7

Set/clean the destination

设置/清理目的地

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

Extract history of each file in email format

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

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 -- "
/tmp/mail/dir
    ├── subdir
    │   ├── file3
    │   └── file4
    └── file5
" > "$historydir/
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
"' {} ';'

Unfortunately option --followor --find-copies-hardercannot be combined with --reverse. This is why history is cut when file is renamed (or when a parent directory is renamed).

不幸的是选项--follow--find-copies-harder不能与--reverse. 这就是为什么在重命名文件时(或重命名父目录时)历史会被删除的原因。

Temporary history in email format:

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

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

Dan Bonacheasuggests to invert the loops of the git log generation command in this first step: rather than running git log once per file, run it exactly once with a list of files on the command line and generate a single unified log. This way commits that modify multiple files remain a single commit in the result, and all the new commits maintain their original relative order. Note this also requires changes in second step below when rewriting filenames in the (now unified) log.

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



2. Reorganize file tree and update filenames

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

Suppose you want to move these three files in this other repo (can be the same repo).

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

/tmp/mail/dir
    └── dirB
        ├── dirB1
        │   ├── file33
        │   └── file44
        └── dirB2
             └── file5

Therefore reorganize your files:

因此重新组织您的文件:

cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:/
my_other_repo
├── dirF
│   ├── file55
│   └── file56
└── dirH
    └── file77
:g" -i "
cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am --committer-date-is-author-date
"' {} ';'

Your temporary history is now:

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

my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB
│   ├── dirB1
│   │   ├── file33
│   │   └── file44
│   └── dirB2
│        └── file5
└── dirH
    └── file77

Change also filenames within the history:

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

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


3. Apply new history

3. 应用新历史

Your other repo is:

你的另一个回购是:

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

Apply commits from temporary history files:

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

git filter-repo --path-rename ParentFolder/FolderwithContentOfInterest/:FolderwithContentOfInterest/ --force

--committer-date-is-author-datepreserves the original commit time-stamps (Dan Bonachea's comment).

--committer-date-is-author-date保留原始提交时间戳(Dan Bonachea的评论)。

Your other repo is now:

你的另一个回购现在是:

git remote add origin [email protected]:MyCompany/MyRepo.git

Use git statusto see amount of commits ready to be pushed :-)

git status看被推提交准备的金额:-)



Extra trick: Check renamed/moved files within your repo

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

To list the files having been renamed:

要列出已重命名的文件:

git pull

More customizations: You can complete the command git logusing options --find-copies-harderor --reverse. You can also remove the first two columns using cut -f3-and grepping complete pattern '{.* => .*}'.

更多自定义:您可以git log使用选项--find-copies-harder--reverse. 您还可以使用cut -f3-和 grepping 完整模式 '{.* => .*}'删除前两列。

git branch --set-upstream-to=origin/history history

回答by Parag Bangad

I followed this multi-step process to move code to the parent directory and retained history.

我按照这个多步骤过程将代码移动到父目录并保留历史记录。

Step 0: Created a branch 'history' from 'master' for safekeeping

第 0 步:从 'master' 创建一个分支 'history' 用于保管

Step 1: Used git-filter-repotool to rewrite history. This command below moved folder 'FolderwithContentOfInterest' to one level up and modified the relevant commit history

第 1 步:使用git-filter-repo工具重写历史记录。下面的这个命令将文件夹 'FolderwithContentOfInterest' 移动到一个级别并修改了相关的提交历史

git push

Step 2: By this time the GitHub repository lost its remote repository path. Added remote reference

第 2 步:此时 GitHub 存储库丢失了其远程存储库路径。添加远程参考

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

Step 3: Pull information on repository

第 3 步:在存储库上拉取信息

$ 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

Step 4: Connect the local lost branch with the origin branch

第四步:连接本地丢失分支和源分支

 $ 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%)

Step 5: Address merge conflict for the folder structure if prompted

步骤 5:如果出现提示,解决文件夹结构的合并冲突

Step 6: Push!!

第 6 步:!!

##代码##

Note: The modified history and moved folder appear to already be committed. enter code here

注意:修改后的历史记录和移动的文件夹似乎已经提交。 enter code here

Done. Code moves to the parent / desired directory keeping history intact!

完毕。代码移动到父目录/所需目录保持历史完整!

回答by John S Gruber

While the core of Git, the Git plumbing doesn't keep track of renames, the history you display with the Git log "porcelain" can detect them if you like.

虽然作为 Git 的核心,Git 管道不会跟踪重命名,但如果您愿意,您与 Git 日志“瓷器”一起显示的历史记录可以检测到它们。

For a given git loguse the -M option:

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

git log -p -M

git log -p -M

With a current version of Git.

使用当前版本的 Git。

This works for other commands like git diffas well.

这也适用于其他命令git diff

There are options to make the comparisons more or less rigorous. If you rename a file without making significant changes to the file at the same time it makes it easier for Git log and friends to detect the rename. For this reason some people rename files in one commit and change them in another.

有一些选项可以使比较或多或少严格。如果您重命名文件而不同时对文件进行重大更改,则 Git 日志和朋友可以更轻松地检测到重命名。出于这个原因,有些人在一次提交中重命名文件并在另一次提交中更改它们。

There's a cost in CPU use whenever you ask Git to find where files have been renamed, so whether you use it or not, and when, is up to you.

每当您要求 Git 查找文件被重命名的位置时,CPU 使用都会产生成本,因此是否使用它以及何时使用取决于您。

If you would like to always have your history reported with rename detection in a particular repository you can use:

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

git config diff.renames 1

git config diff.renames 1

Files moving from one directory to another isdetected. Here's an example:

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

##代码##

Please note that this works whenever you are using diff, not just with git log. For example:

请注意,这在您使用 diff 时都有效,而不仅仅是使用git log. 例如:

##代码##

As a trial I made a small change in one file in a feature branch and committed it and then in the master branch I renamed the file, committed, and then made a small change in another part of the file and committed that. When I went to feature branch and merged from master the merge renamed the file and merged the changes. Here's the output from the merge:

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

##代码##

The result was a working directory with the file renamed and both text changes made. So it's possible for Git to do the right thing despite the fact that it doesn't explicitly track renames.

结果是一个工作目录,文件重命名,两个文本都进行了更改。因此,尽管 Git 没有明确跟踪重命名,但它仍有可能做正确的事情。

This is an late answer to an old question so the other answers may have been correct for the Git version at the time.

这是对旧问题的较晚答案,因此当时其他答案对于 Git 版本可能是正确的。

回答by Jakub Pawlowski

First create a standalone commit with just a rename.

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

Then any eventual changes to the file content put in the separate commit.

然后将文件内容的任何最终更改放入单独的提交中。