我有两个存储库,然后将它们合并为一个,因此每个存储库现在都复制到新/第三个存储库中的分支。 我还使用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 具有在存储库中形成向后提交链的效果。 如果我们从一个几乎空的仓库开始,其中只有三个提交,并称它们为A
、B
和C
,而不是使用它们实际丑陋的大哈希 ID,我们可以像这样绘制它们:
A <-B <-C
提交C
是最新的,它会记住其父B
的实际哈希 ID。 提交B
是我们第二个做的,它会记住其父A
的实际哈希 ID。 提交A
是第一个,作为第一个,没有以前的提交需要记住,所以它是根提交。
如果提交哈希 ID 真的这么简单,我们可以记住有三个提交,所以我们最新的必须C
。 但它们的外观和行为都像随机数,我们人类不可能记住它们。 所以我们让 Git 使用分支名称来记住链中的最后一次提交:
A <-B <-C <-- master
名称master
记住实际的哈希 ID,以便名称master
指向提交C
。 同时,C
指向B
,B
指向A
。
如果我们想添加新的提交,我们告诉 Git 使用名称提取提交C
master
. 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
上的工作(提交H
和J
之间的更改)与dev
上的工作,提交H
和L
之间的更改。 如果 Git 能够自己完成所有合并,Git 也会自行进行最终合并提交。 这个新的合并提交M
作为它的第一个父级,提交J
:我们已经签出的提交。 它有 提交L
,我们告诉 Git 合并的那个,作为它的第二个父级。 与往常一样,Git 将新提交的新哈希 ID 写入当前分支名称,以便master
现在指向新的合并M
:
I--J
/
...--G--H M <-- master
/
K--L <-- dev
如果我们愿意,我们现在可以删除名称dev
,因为提交都可以通过从M
开始并向后工作来找到。 从M
开始,我们必须倒退J
和L
。
请注意,如果我们保留名称dev
,则通过H
提交,加上K
和L
,都在两个分支上。dev
中的所有提交现在也都在master
中。 提交I
、J
和M
只在master
中,但其他的在两个分支中。 在合并之前,通过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-branch
refs/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。 您现在拥有重建的历史,完全按照您想要的方式。