Git在保留历史记录的同时将目录从同一存储库中的一个分支移动到另一个分支吗?

时间:2019-05-28 07:29:11

标签: git

如何在保存历史记录的同时将目录从一个分支移动到另一个分支?

我想搬进同一个仓库。

1 个答案:

答案 0 :(得分:1)

TL; DR

您不能(完全)得到想要的东西,但是,如果有时尝试,您可能会发现,您会得到所需的东西。

您要做的就是重命名目录并提交。如果当前分支顶部的提交中尚未包含该目录,则可能首先需要从其他提交中提取目录。也就是说,您可能需要一个缩写:

git checkout otherbranch -- path/to/directory
git commit                                      # optional, but see below

,然后在任何情况下运行:

git mv path/to/directory new/path/to/dir

,然后git commit结果。那并不会满足您的需求,但可能会满足您的需求,特别是如果您进行了第一次不需要的东西时,重命名,以便您有两个相邻的提交,一个带有旧名称,另一个带有新名称。

相反,您可能想合并分支,提交合并,然后才重命名并再次提交。是否需要以及为什么需要详细说明。

在这里了解两件事很重要:

  • Git仅存储文件,而不存储目录。 1 每个提交都存储项目中所有文件的完整快照。
  • 在Git中文件没有历史记录。在Git中,提交 历史记录。

人们经常反对Git存储快照的想法,因为git log -p显示了补丁,即更改。例如,您查看了提交0a36ca1,并且看到对README.md的更改。然后git log继续提交0a36ca1的父提交922bf37,例如,您看到README.md和/或其他文件的另一处更改,依此类推。这是否意味着0a36ca1仅将更改存储到README.md?答案是:否,0a36ca1存储README.md 和所有其他文件的完整副本。 Git通过检查两者 922bf370a36ca1的父元素,即0a36ca1之前的提交)来显示的变化— < em>和 0a36ca1。两次提交都有每个文件的副本。 Git比较了两个提交的文件。这两个提交中的所有文件都与README.md except 匹配。然后,Git比较了两个README.md版本以查看发生了什么变化,并向您显示了该文件中发生了什么更改

git show命令与之类似,不同之处在于您通常为它提供一次提交的哈希ID,而git show打印该提交的元数据(创建记录的元数据,创建者,时间和原因-日志消息)。然后将父级中的快照与该提交中的快照进行比较。 不同是您看到的文件。

当您通过运行git loggit log来询问git log master的历史记录时,Git:

  1. 从当前提交(git logmaster中的 last 提交开始,并向您显示提交git show的消息。 2
  2. 然后,移回刚刚显示的提交的父级。在合并提交时,这变得很复杂,但是现在,只需考虑一个简单的线性链即可。

重复此过程,直到Git的父母用完为止,或者您厌倦了通过git log输出进行分页。给定一个不错的简单线性提交链,例如:

A <-B <-C <-D <-E <-F <-G   <-- master

(单个大写字母代表Git实际使用的丑陋的哈希ID),Git首先显示G(名称为master),然后移至{ {1}}并向您显示F,然后移至F并显示E,依此类推。提交E是存储库中的第一个提交-它没有父级; A中没有向后的箭头让Git向左移动-因此A表示它是从头开始创建的每个文件。这意味着git show以相同的方式显示它。当然,没有父母,就没有向后跟随的箭头。


1 从技术上讲,目录会变成内部的 tree 对象,但是Git不会存储 empty 目录,原因很简单,因为您可以不能将目录添加到Git的索引中,并且Git不会从工作树中构建提交,而是从索引中构建。将Git视为仅存储文件会更容易,因为这是最终的结果。

2 当然,这假设您正在使用git log -pgit log -pgit log之间有几个重要的区别:首先,git show向后走;第二,git log默认为 not 显示补丁;第三,git log以不同的默认方式显示合并提交:git show默认对自己说: ugh,一个合并提交,太难了:我只打印日志消息,然后继续前进,根本不显示任何差异。 git log -p的默认设置是显示组合差异,这是对多个父母的差异化形式。


git show可以显示历史记录的子集

您可以运行以下命令,而不是仅运行git loggit log

git log master

,然后查看git log master -- path/to/file.ext 的历史记录。 path/to/file.ext在这里的操作与往常一样是 commit 历史,但随后不显示一些提交。也就是说,鉴于上面的简单线性链,git log从提交git log开始。它比较GF(的快照)以查看更改了哪些文件。如果这些文件 do 包含G,则path/to/file.ext显示提交git log。然后,即使它什么都没显示,它又返回到提交G

换句话说,F不仅可以向您显示遍历的所有所有提交,而且可以向您显示遍历的所选提交。结果是,Git似乎具有文件历史记录,但事实并非如此:它只是合成真实历史记录中的一个子集历史记录,并且可以正常工作。

这很重要,因为当Git进行此合成文件历史记录创建时,git log正在修改提交步。 The git log documentation calls this History Simplification,并且很复杂。有大约六个git log选项可以控制如何简化历史记录。这意味着您在git log中看到的“文件历史记录”取决于您传递给git log选项,以及实际的提交历史记录。

(至少有一天要阅读和研究“历史简化”部分,因为其中有很多内容。我使用Git已有很长时间了,我想认为我对此有很多了解,但是即使到那时我必须重新参考文档。尤其是“ TREESAME”的概念(在每次提交中减去不需要的树组件之后才应用),并且合并时遵循哪些提交是特别棘手的。)

使用git log--follow将尝试检测重命名

当Git执行此逐提交,向后遍历一连串提交的过程时,父级到子级之间的差异可能表明某些文件已被重命名。例如,名为git log的文件可能已重命名为名为README的文件。一个简单的:

README.md

将向您展示git log master -- README.md 随时间的变化(向后),但是在README.md被命名为README.md时停止,因为它正在寻找README并从这里开始提交没有 README.md

当您将README.md添加到--follow时,将在整个重命名之后,只需更改一个哪个文件,该文件就只能使用一个文件!寻找。在检测到例如git logD的边界后,现在E的文件在落实README.md中被称为READMED停止寻找git log,并从README.md开始寻找对名为D的文件的更改。真的就是这么简单。

README对于您的用例来说太简单了

这里的问题是--follow 很简单,这太简单了。因此,它不会执行您想要的操作,原因有两个:

  • 首先,您要谈论的是跨相当大的距离复制文件:

    --follow

    如果您的完整文件目录位于...--F--G--H <-- master \ N--O--P <-- branch 上的提交H中,而您刚刚将其复制到新的提交中,则将在master上进行是在提交branch之后发出的,嗯,没有从 PP的反向链接。这就是为什么我建议您提交文件而不重命名它们,然后重命名它们并再次提交。结果将是:

    H

    其中提交...--F--G--H <-- master \ N--O--P--Q--R <-- branch 的文件已重命名,R的文件未重命名,只是从Q复制而来。在针对H的提交 log消息中,您可以声明指向指向Q时,已经从分支master复制了整个目录(请在此处使用H的真实哈希ID-运行H以查看git rev-parse master立即指定的哈希ID)。然后,每当Git从提交master返回到提交R时,您便重新命名目录并再次提交以使它们显示为重命名。

  • Q选项仅适用于一个文件。也就是说,给定的提交是git log --follow或从其派生的,因此具有新的目录名称,则必须运行:

    R

    最终将以提交git log --follow [<commit-hash>] [--] new/path/to/dir/file.ext 的方式工作,显示R(因为与提交new/path/to/dir/file.ext相比,它在提交R中已重命名),然后移回提交Q并开始寻找Q

从检测到的单个重命名,加上path/to/directory/file.extQ中的日志消息,您(一个聪明的人,而不是一个只遵循非常简单的规则的愚蠢的Git程序)可以得出结论,这些文件的全部来自提交R

这是您可能想要真正合并的地方。您不仅可以从H复制文件,还可以将提交H作为 merge commit ,将历史记录从提交Q重新连接到 都提交:Q P。也就是说,假设您最终得到:

H

现在,当Git遍历提交历史时,它会变为:...--F--G--H <-- master \ \ N--O--P----Q--R <-- branch RQ和and HP和and {{ 1}},G和-O,依此类推。也就是说,F遍历了实际的历史记录,一次完成了一次提交,并通过一种复杂的方法来跟踪历史记录中的分叉,其中提交Ngit log合并形成提交H

进行合并的缺点很明显:这是合并。默认情况下,它将引入某些共同祖先以来的 all 更改-因为在PQ之前NF之前的任何提交最终导致返回 shared 提交:这是两个分支上的提交。您不一定必须 commit 进行这些更改,甚至不必进行任何更改:您可以使提交branch的快照与提交master的快照匹配,当然除了您想要的新目录。

(进行这种合并的方法有多种。如何完全实现合并是另一个StackOverflow问题,已经很好地回答了这个问题。请参见(Git Merging) When to use 'ours' strategy, 'ours' option and 'theirs' option?VonC's answer to a different question here。这里有很多选择,但是如果您完全想使用Q,则可能要从P开始,然后使用git merge -s ours --no-commit提取文件,然后再将-s ours作为合并。)

合并的优点是将历史联系在一起,这样git checkout <commit> -- <path> 可以从合并Q到提交git log, (重新命名)文件的实际历史记录的来源。缺点是它将历史联系在一起,因此从那时起,Git认为将QH合并的正确结果是H,甚至如果您以后改变主意。

如果合并不是您想要的,则可能需要提交加上日志消息。