Git 变基合并分支与不同的子文件夹



我有两个存储库,然后将它们合并为一个,因此每个存储库现在都复制到新/第三个存储库中的分支。 我还使用filter-branch将所有文件从一个存储库移动到子文件夹中,将另一个文件移动到另一个子文件夹中。

因此,branch_A在文件夹"A"下,branch_B"在文件夹B"下,从git的角度来看,一直都是这样。

我想将两个分支合并为一个,这样当我从历史记录中间签出随机提交时,我将拥有两个文件夹。

我可以通过变基来做到这一点,但至关重要的是,我希望提交保留其作者日期,基本上将历史记录交织在一起。当我变基时,它只是在最后删除提交,并且所有提交都有今天的日期。

这应该是可行的,因为当分支跟踪不同的文件夹时,绝对没有冲突。

我该怎么做?

谢谢。

这是可以做到的,是的:但这并不简单,仅靠变基并不能让你到达那里。

(我敢肯定,其中一堆是你已经知道的东西,但值得一步一步地回顾,因为很多 Git 文档还有很多不足之处,这在人们的理解中留下了空白。

请记住,存储库是提交的集合。 每个提交都是(或包含,如果我们将其分解,但现在让我们将提交视为完整的实体)源树的快照,即一些数据,以及有关该快照的一些信息,即一些元数据。 提交本身由唯一的哈希 ID 标识。 此哈希 ID 只不过是提交内容的加密校验和。1校验和本身不能改变,因为它只是该字符串的校验和,而字符串本身不会改变,因为这就是构成提交的原因。

现在,至关重要的是,在每个提交的元数据中,有一行parent

$ git cat-file -p HEAD | grep parent
parent 90d79d71910415387590a733808140e770382b2f

也就是说,每个提交都包含其他某个先前提交的实际哈希 ID。一些提交(合并提交)包含两个或多个父 ID,并且至少一个提交(提交)没有父 ID,但大多数提交只有一个。

这些父哈希 ID 具有在存储库中形成向后提交链的效果。 如果我们从一个几乎空的仓库开始,其中只有三个提交,并称它们为ABC,而不是使用它们实际丑陋的大哈希 ID,我们可以像这样绘制它们:

A <-B <-C

提交C是最新的,它会记住其父B的实际哈希 ID。 提交B是我们第二个做的,它会记住其父A的实际哈希 ID。 提交A是第一个,作为第一个,没有以前的提交需要记住,所以它是根提交。

如果提交哈希 ID 真的这么简单,我们可以记住有三个提交,所以我们最新的必须C。 但它们的外观和行为都像随机数,我们人类不可能记住它们。 所以我们让 Git 使用分支名称来记住链中的最后一次提交:

A <-B <-C   <-- master

名称master记住实际的哈希 ID,以便名称master指向提交C。 同时,C指向BB指向A

如果我们想添加新的提交,我们告诉 Git 使用名称提取提交Cmaster. Git 记得我们在分支master上,通过将特殊名称HEAD附加到名称master

A--B--C   <-- master (HEAD)

如果我们想向新分支添加新的提交,我们告诉Git:dev创建一个新的分支名称,也指向提交C,并将HEAD附加到dev现在我们有:

A--B--C   <-- dev (HEAD), master

现在我们以通常的方式进行新提交。 Git 通过创建源快照、添加我们的名称和电子邮件以及日期等来创建提交,并使用提交C的哈希 ID 作为新提交的父级。 新的提交会得到一个新的,看似随机的(但实际上完全确定的,一旦我们有了日期和时间戳以及我们的日志消息等)哈希ID。 不过,我们只称其为提交D

A--B--C

D

神奇的部分现在发生了:Git 通过将提交D的实际哈希 ID写入当前分支名称(特殊名称HEAD附加到的分支名称)来更新该分支名称。 所以现在名称dev指向D,而master继续指向C

A--B--C   <-- master

D   <-- dev (HEAD)

随着我们添加更多提交,在某些时候我们会遇到这样的情况:

I--J   <-- master
/
...--G--H

K--L   <-- dev

我们运行git checkout master,它选择提交J并将HEAD附加到master,然后git merge dev。 合并操作现在结合了master上的工作(提交HJ之间的更改)与dev上的工作,提交HL之间的更改。 如果 Git 能够自己完成所有合并,Git 也会自行进行最终合并提交。 这个新的合并提交M作为它的第一个父级,提交J:我们已经签出的提交。 它有 提交L,我们告诉 Git 合并的那个,作为它的第二个父级。 与往常一样,Git 将新提交的新哈希 ID 写入当前分支名称,以便master现在指向新的合并M

I--J
/    
...--G--H      M   <-- master
    /
K--L   <-- dev

如果我们愿意,我们现在可以删除名称dev,因为提交都可以通过从M开始并向后工作来找到。 从M开始,我们必须倒退JL

请注意,如果我们保留名称dev,则通过H提交,加上KL,都在两个分支上。dev中的所有提交现在也都在master中。 提交IJMmaster中,但其他的在两个分支中。 在合并之前,通过H提交的提交都在两个分支中。


1从技术上讲,它是文字单词commit的校验和,元数据的大小(以字节为单位),包括数据的树哈希 ID 字符串、一些空格和其他字节,然后是元数据的字节。 使用git cat-file -p HEAD查看一个示例提交:校验和是在给定格式化指令时 Python 将打印的内容作为前缀的结果:

b'commit {}{}'.format(len(content), content)

其中content包含git cat-file -p产生的字节字符串。


从单独的历史记录中获取会创建第二个根

让我们回到一个很好的简单三提交原始存储库,只有一个分支:

A--B--C   <-- master

现在,让我们从另一个不同的三提交存储库git fetch,该存储库也具有一个master。 这让我们otherrepo/master,如果我们使用名称otherrepo作为遥控器的名称:

git remote add otherrepo <url>
git fetch otherrepo

结果在:

A--B--C   <-- master
D--E--F   <-- otherrepo/master

我们可以为第二个分支创建自己的分支名称,而不是使用这个远程跟踪名称otherrepo/master。 这并不重要,但它使我们的下一个git filter-branch更容易,所以让我们这样做:

git branch m2 otherrepo/master

现在,我们将运行将所有内容移动到子目录中的git filter-branch命令。 每个此类命令都将一些原始提交复制到具有不同哈希 ID 和不同保存快照的新提交,但具有相同的作者和提交者信息以及相同的日志消息。 父哈希 ID 指向副本,因此我们最终得到:

A--B--C   [abandoned]
A'-B'-C'  <-- master
D--E--F   [abandoned]
D'-E'-F'  <-- m2

我们不再需要原始的提交链,一旦我们放弃了git filter-branchrefs/original/名称,我们甚至无法再找到原始的,除非我们在某处写下它们的哈希 ID。 因此,如果我们愿意,我们可以停止绘制它们,我会的(但我会在一个字母的提交名称上保留刻度/素数标记,以表示这些是保存重命名为子树文件的那些)。

你现在需要的是发明所需的最终图表

您现在拥有:

A'-B'-C'  <-- master
D'-E'-F'  <-- m2

您可能希望拥有:

A"-E"-B"-F"   <-- master

或者,也许您希望拥有:

A"-C"-F"   <-- master

或者,也许这完全是另一回事。很明显,但可能不完全清楚,也许实际上不是真的,你想要一个新的根提交,可能是这个A",这是以下结果:

  • A'中的树D'中的树提取到一个公共树中
  • 提交它,使用来自A'D'或两者的日志消息

在此新A"提交之后,您可能希望D'子树替换为E'子树。 或者,您可能想用B'子树替换A'子树,并将D'子树替换为E'子树。 或者,也许你想要第三种组合——这部分根本不清楚,当然对我不清楚,也许对你来说也不是。:-)

但是,无论如何,您的工作是确定要组合哪些子树,以及要组合或使用哪些提交消息。 然后,您可以从该结果进行新的提交。 这将是您的第二次提交,它将A"作为其父级。

这将重复链中尽可能多的提交。 如果链很复杂——如果master指向有背离和合并的东西,和/或m2指向有背离和/或合并的东西,你必须弄清楚你想如何组合这两个历史,包括它们的子树。 提取任何master提交肯定只会影响A子树,而提取任何m2提交只会影响B子树。 但实际上以任何顺序进行提交,并与任何父级进行新提交......这是你必须解决的艰巨工作。

一旦你弄清楚你想要什么,就要实现它,这是一个相对简单的编程问题。 您可以尝试使用git filter-branch来执行此操作,但只执行git filter-branch操作可能更容易:设置正确的树并进行新的提交,使用git write-tree写出索引,然后在环境中使用正确的标准输入日志消息(或带有日志消息的-F文件)和正确的GIT_{AUTHOR,COMMITTER}_{NAME,EMAIL,DATE}字符串进行git commit-tree -p <parent> [-p <parent> ...] <tree>,以强制作者和提交者姓名, 电子邮件地址以及新提交的日期和时间戳。 管道命令git commit-tree将进行新的提交,并在其标准输出上生成该新提交的哈希 ID。 酌情将其用作后续提交的父级,以构建新链。

(请注意,您可以使用git read-tree来填充git write-tree的索引,和/或者您可以以更典型的方式将提交中的文件提取到工作树中,然后使用git add构建要写入的新索引。 请参阅过滤器分支源代码 - 它只是一个巨大的 shell 脚本 - 了解一些技巧和技术。

完成所有操作并构建了所需的分支提示提交后,使用git update-ref或更面向用户的git branch命令强制设置特定的一个或多个分支名称,以保存所需提交的哈希 ID。 您现在拥有重建的历史,完全按照您想要的方式。

最新更新