在复制 git 存储库后,如何取回共享历史记录?

How do I get shared history back after a git repository has been copied?

很久很久以前,在很远的办公室里,有人复制了一个 github 存储库并将其上传到 Visual Studio Team Services (VSTS)。我们开发人员愉快地编写代码,开发功能并修复 VSTS 中的错误。现在是时候将我们的代码重新发布到开源社区的怀抱中了...

不幸的是,我们的 VSTS 存储库与 github 存储库没有共享历史记录,因为它是副本,而不是克隆。虽然我们可以将 github 存储库添加为远程,但将我们的代码合并回主要分支是一个令人讨厌的冲突。整个文件夹结构已被移动或重命名,开源开发人员已提交对 github 存储库中的这些文件的更改。

有什么办法可以让我们的分支回到原来的地方吗?就像在复制存储库时将我们的整个分支树重新定位到 github 上的最后一次提交?

我想出的最好办法是将 VSTS 中的每个 CL 挑选到 github 上,这听起来像是一些严肃的侦探工作,以确定在何处插入重命名。

假设 VSTS 存储库是一个 Git 存储库,您可以:

  • 克隆您的 GitHub 存储库
  • 从正确的提交创建一个新分支
  • 使用 VSTS 分支第一次提交的镜像副本覆盖工作树内容(以避免任何冲突解决)。然后添加并提交。
  • git 从 VSTS 中挑选(添加为远程并获取)您的 VSTS 主分支的所有提交到新的本地分支(无冲突)
  • 将新分支推回到 GitHub 仓库

这——将非克隆与实际克隆相结合——通常是困难的。

让我们写一个理论例子,以git://github.com/repo为原型。假设 ssh://example.com/copy.git 将代表您使用以下命令序列设置的存储库:

<download tarball or zip file from github.com/repo>
<extract tarball or zip file into directory D>
$ cd D
$ git init
$ git add .
$ git commit -m initial -m "" -m "imported from github.com/repo.git"

之后,您从这个独立的存储库创建了位于 ssh://example.com/repo.git--bare 存储库。

现在已经过了一段时间,您已经意识到您想要使用 github.com/repo.git 的实际克隆。唉,你的 ssh://example.com/repo.gitgit://github.com/repo.git 没有共享的历史——没有共同的提交。 运行:

$ git clone ssh://example.com/repo.git combine
$ cd combine
$ git remote add public git://github.com/repo.git
$ git fetch public

获得所有 public 提交,但是尝试将 public/master 与您自己的私人 master 合并是一团糟。

在某些非常具体的情况下,修复此问题实际上并不难。诀窍在于将 root commit 现在位于 combine 存储库中,可从 master 访问,与 combine 存储库中的所有提交进行比较可从所有 public/* 个远程跟踪名称访问。如果你幸运的话,正好有一个提交的 tree 与你自己的根提交的 tree 完全匹配,因为你得到的 tarball-or-zip-file 生成了一个相同的树。

如果你幸运的话,没有这样的提交。在这种情况下,您也许可以找到 "sufficiently close" 的提交。但是让我们假设您确实找到了一个提交,可从public/master访问,它与您自己的根提交完全匹配:

A--B--...--o--o   <-- master (HEAD), origin/master
        \
         ... (there may be other branches)

C--...--R--...--o   <-- public/master

在这里,大写字母 A 代表您自己的根提交的实际哈希 ID——您从下载的 tarball 或 zip 文件中创建的那个——而 B 只是提交在那之后。 C 代表可从 public/master 到达的(或某些)根提交,主要在图中仅用于说明:我们可以肯定的是,至少还有一个这样的根(无父)提交. 字母R代表与您的提交A完全匹配的提交,这是目前最有趣的提交。

我们现在想做的是假装 第二个-最有趣的提交的父项,B , 是提交 R 而不是提交 A。我们做得到! Git 有一个名为 git replace 的设施。 git replace 所做的是 复制 一个对象,同时进行一些更改。在我们的例子中,我们想要的是将提交 B 复制到一些新的提交 B' 中,它看起来几乎与 B 完全一样,但有一点改变了:它的父级。我们希望 B' 列出提交 R.

的哈希 ID,而不是将提交 A 的哈希 ID 列为 B' 的父级

换句话说,我们将有:

A---------B--...--o--o   <-- master (HEAD), origin/master

          B'
         /
C--...--R--...--o   <-- public/master

现在我们要做的就是说服 Git 当它查找提交 B 时,它应该注意到有这个 替换 提交,B',并迅速将目光从 B 上移开,转而看向 B'。这就是 git replace 所做的其余部分。因此,在找到提交 RB 之后,我们 运行:

git replace --graft <hash-of-B> <hash-of-R>

现在 Git 假装 图形显示为:

          B'-...--o--o   <-- master (HEAD), origin/master
         /
C--...--R--...--o   <-- public/master

(嗯,Git 假装这个,除非我们 运行 git --no-replace-objects 看到现实)。

大的或小的缺点

除了定位提交 R 的相当艰巨的工作之外——查找 AB 非常容易,它们是 git rev-list --topo-order master 列出的最后两个哈希 ID ——这个 git replace 把戏有一个缺陷。替换提交 B' 现在存在于我们的存储库中,但它通过特殊名称 refs/replace/<em>hash <em> 定位</em> </em>,其中 hash 是原始提交 B 的哈希 ID。默认情况下,此替换对象(及其名称)不会发送到新克隆

您可以制作 do 具有替换对象及其名称的克隆,并使用它们,一切正常。但这意味着每次有人克隆您的 combine 存储库时,他们必须 运行:

git config --add remote.origin.fetch '+refs/replace/*:refs/replace/*'

或类似规则(此特定规则只是将您的克隆的 refs/replace/ 命名空间从属于 origin 的命名空间,这是粗糙但有效的)。

或者,您可以声明一个 flag day and run git filter-branch or similar to cement the replacement in place. I have described this elsewhere, though the best I can find at the moment is my answer to How can I attach an orphan branch to master "as-is"? 实际上,您创建一个 new 存储库,其中包含 B' 而不是 B,但不有 A,并且有 每个作为 B' 后代的提交的新副本(除了父哈希 ID 外,内容相同)。然后,您让所有用户从旧 repo.git 切换到新用户。这很痛苦,但只有一次。

如果您不打算长期使用组合存储库,这可能无关紧要。

除上述之外,您还可以使用嫁接历史来产生合并——Git命令通常会跟随替换——之后你可能不需要替换移植提交。在这种情况下,缺点是短暂的:它只会持续到您合并代码为止。