如何将Git中的一个目录从一个分支移动到另一个分支,同时保留历史记录?

5
如何在保留历史记录的情况下,将git目录从一个分支移动到另一个分支?
我想在同一仓库中进行移动。

我认为这个问题以前已经被问过了,不管怎样,这就是你要找的内容:https://dev59.com/T1gR5IYBdhLWcg3wDJpN - Ishara Lakshitha
3
可能是重复的问题:如何将一个Git分支中的单个文件版本复制到另一个分支?(原文链接:https://dev59.com/S3VC5IYBdhLWcg3wZwNT) - jpeg
1
不,上面的链接是从一个代码库移动到另一个代码库。我想要从一个分支移动到另一个分支。 - shruti hegde
@shrutihegde 不,链接的问题(由jpeg提出)是处理同一存储库中的分支。我鼓励你重新阅读它。 - Romain Valeri
1个回答

6

简述

你可能无法完全得到你想要的,但如果你尝试一下,你可能会发现你得到了你所需要的。

你只需重命名目录并提交即可。如果目录不在当前分支的最新提交中,则您可能需要从其他提交中提取目录。也就是说,您可能需要进行初始操作:

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来显示更改。这两个提交都有每个文件的副本。Git比较了这两个提交的文件。那两个提交中的所有文件都匹配,除了README.md。然后Git比较了这两个README.md版本,以查看发生了什么变化,并向您展示了该文件中发生了什么变化。 git show 命令类似,但通常您需要给出一个提交的哈希 ID,git show 将打印提交的元数据(谁制作的、何时以及为什么——日志消息),然后将父级中的快照与该提交中的快照进行比较。不同的是,您所看到的文件。
当您使用 git log 请求历史记录时,Git 会执行以下操作:git loggit log master
1. 从当前提交开始(git log),或者从master的最后一次提交开始,并像 git show 一样显示该提交2。 2. 然后,移动回刚刚显示的提交的父级。在合并提交时,这变得复杂,但现在只需考虑一个简单的线性链即可。
这个过程会重复进行,直到 Git 没有更多的父级,或者您已经翻阅完了 git log 的输出。假设存在一个简单的线性提交链,如下所示:
A <-B <-C <-D <-E <-F <-G   <-- master

(这里单个大写字母代表Git实际使用的复杂哈希ID),Git首先展示给您G(按名称master 找到),然后移动到F并显示F,然后移动到E并显示E,以此类推。提交A是存储库中的第一个提交-它没有父提交;没有向左移动的箭头从A出来让Git向左移动-因此git show 将其显示为每个文件都是从头开始创建的。这意味着git log-p 以相同的方式显示它。当然,由于没有父提交,因此没有箭头可以向后跟随。

1技术上,目录转化为内部的树形对象,但Git不会存储一个目录,因为你无法将目录放入Git的索引中,而Git不是从工作树,而是从索引中构建提交。更容易地,把Git想象成只存储文件,因为这是最终效果。

2当然,这假设您正在使用git log -pgit loggit show之间有几个重要的区别:首先,git log进行反向行走; 其次,git log默认情况下显示补丁; 第三,git show以不同的默认方式显示合并提交:git log -p默认为自己说:啊,合并提交太难了:我只打印日志消息并继续前进,而不显示任何差异。 这里git show的默认值是显示组合差异,它是针对多个父级的缩小形式的差异。


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

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

git log master -- path/to/file.ext

看起来是关于path/to/file.ext的历史记录。这里git log的作用是像往常一样遍历提交历史,但是它不显示某些提交。也就是说,对于上面的简单线性链,git log从提交G开始。它比较(快照)FG以查看哪些文件发生了变化。如果这些文件包括path/to/file.ext,则git log会显示提交G。然后它会回到提交F,即使它什么都没显示。
换句话说,git log不仅可以显示它遍历的所有提交,还可以显示所选的提交。结果是,Git似乎有文件历史记录,但它实际上没有:它只是根据实际历史记录合成了一个子集历史记录,并在处理过程中工作。
这很重要,因为当Git进行合成文件历史创建时,git log正在修改提交步骤。git log文档将其称为历史简化,它很复杂。有大约半打git log选项来控制如何执行历史简化。这意味着您使用git log看到的“文件历史记录”取决于您传递给git log的选项以及实际提交历史记录是什么。
(至少有一天要阅读和学习历史简化部分,因为其中有很多内容。我已经使用Git很长时间了,并且认为我对它了解很多,但即使如此,我也不得不参考此文档。特别是“TREESAME”的概念-在从每个提交中减去不需要的树组件后应用,并且在合并时跟随哪些提交尤其棘手。)

使用--follow参数,git log将尝试检测文件重命名操作

由于Git是逐个提交地向后遍历一系列提交,因此从父提交到子提交的差异可能显示某些文件已被重命名。例如,名为README的文件可能已被重命名为README.md。可以简单地使用以下命令:

git log master -- README.md

将向您展示如何逆向演变README.md,但会在README.md被命名为README时停止,因为它正在寻找README.md,而此后的提交不再具有README.md。当您在git log中添加--follow时,它将跟踪该文件-它只适用于一个文件!-在重命名过程中,只需更改它正在查找的文件即可。例如,在检测到从提交-D-到-E边界时,现在称为README.md的文件在提交D中被称为READMEgit log停止寻找README.md并开始从D开始寻找名为README的文件的更改。就是这么简单。--follow对于您的用例太简单了。
这里的问题在于--follow实在是太简单了,这种简单过度的情况导致它无法满足你的需求,原因有两个:
  • First, you're talking about copying files across some fairly large gap:

    ...--F--G--H   <-- master
     \
      N--O--P   <-- branch
    

    If your directory-full-of-files is in commit H on master, and you're just now copying it to a new commit you'll make on branch that comes after commit P, well, there's no backwards link from P to H. That's why I proposed that you commit the files without renaming them, then rename them and commit again. The result will be:

    ...--F--G--H   <-- master
     \
      N--O--P--Q--R   <-- branch
    

    where commit R has the files renamed, and Q has them not-renamed, just copied from H. In the commit log message for Q, you can state that the entire directory has been copied from branch master at a time when it pointed to commit H (use H's real hash ID here—run git rev-parse master to see what hash ID master specifies right now). Then you rename the directory and commit again to make them show up as renames, whenever Git walks from commit R back to commit Q.

  • The git log --follow option only works on one file. That is, given a commit that is or descends from R, and therefore has the new directory name, you must run:

    git log --follow [<commit-hash>] [--] new/path/to/dir/file.ext
    

    which will eventually work its way to commit R, show new/path/to/dir/file.ext (because it is renamed in commit R as compared to commit Q), then move back to commit Q and start looking for path/to/directory/file.ext.

从这个检测到的重命名,再加上QR中的日志信息,你——一个聪明的人而不是一个只遵循非常简单规则的愚蠢的Git程序——可以得出结论,啊哈,所有这些文件都来自提交H

这就是你可能需要一个真正的合并的地方。你可以直接将提交Q作为合并提交,将历史记录从提交Q连接回到两个提交:PH。也就是说,假设你最终得到:

...--F--G--H   <-- master
 \          \
  N--O--P----Q--R   <-- branch

现在,当Git遍历提交历史时,它会按顺序遍历R、Q、H和P、G和O、F和N等提交。也就是说,git log会逐个提交地遍历实际历史,通过一种复杂的跟踪方法来追踪历史中的分支,其中提交H和P合并形成提交Q。
这种合并的缺点显而易见:它是一个合并。默认情况下,它将带入自某个共同祖先以来的所有更改,自分支和主分支最终回到共享提交之前的提交N和提交F。您不一定要提交这些更改,甚至不需要任何更改:除了您想要的新目录外,您可以使提交Q的快照与提交P的快照相匹配。

有多种方法可以进行合并。如何实现是另一个完全不同的StackOverflow问题,已经得到了很好的解答。请参见(Git Merging) When to use 'ours' strategy, 'ours' option and 'theirs' option?,以及VonC's answer to a different question here。这里有许多选项,但您可能希望首先使用git merge -s ours --no-commit,如果您想使用-s ours,然后使用git checkout <commit> -- <path>提取文件,最后将提交Q作为合并。

合并的优点在于将历史记录绑定在一起,这样git log就可以从合并Q回到提交H,这是(重命名前)文件实际历史的源头。缺点是它将历史记录绑定在一起,因此从那时起,Git认为将HP合并的正确结果是Q,即使您以后改变了主意。
如果合并不是您想要的,那么提交和日志消息可能就是您所需要的。

我将在今晚和之后的时间里唱滚石乐队的歌曲。感谢这个答案^wbook :D - Zeitounator

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接