克隆 git-svn 存储库导致 "disappearing" 个分支

Cloning a git-svn repository leads to "disappearing" branches

前言

我们有一个很大的 SVN 存储库(200k+ 提交和数百个分支和标签)。一个巨大的、不祥的、无法维护的、令人沮丧的混乱局面。为了更高效地工作,大约一年前我在我的开发机器上做了一个 git svn clone,所以我在本地开发 GIT 然后推送到 SVN。

我们现在正在考虑拆分存储库并将主要开发分支移动到 git,或者至少将我们的开发分支移动到 git。

因为我有我的本地 git 存储库,我想通过克隆它的一部分并推送到我们公司的 Git 实验室来做一些测试,但没有成功,可能是因为我缺乏一些Git机制的知识

开始吧

为了在不推送整个 30GB 存储库的情况下进行一些快速测试,我想对本地 Git 存储库进行浅克隆并使用以下命令推送克隆:

git clone --depth=1 --no-single-branch file:///path/to/repo

我想克隆每个分支的 HEAD 版本,但是克隆的只有 master 分支和我们的开发分支,没有别的(我不确定标签,我没有检查)。过了一会儿,我意识到克隆只包含我们的开发分支,因为它是我唯一检出的分支(即使 git svn 存储库是 SVN 存储库的完整克隆)。

然后我尝试做一个

git clone file:///path/to/repo

我又只得到了 master 和我的开发分支,没有别的。

在这两次尝试中,我注意到克隆版本 (200-700MB) 比原始 git 存储库 (30GB) 小得多。在第二次尝试中,我期待一个与原始存储库大小相同的存储库。

所以我意识到 git 只克隆已签出的分支,而不是远程分支 (remotes/svn/*)。为什么,因为 git svn repo 是 svn repo 的完整副本?为什么不克隆所有分支?它们在那里(否则 git svn 存储库不会那么大),它们只是没有被检出。还有...我们怎么能谈论 "remote" 个分支?它们不是 git svn 存储库的一部分,应该被认为是本地的吗?

那么我如何告诉 git 在克隆 git svn 存储库时考虑所有这些分支?我不想对 git svn 存储库中的所有分支进行大量检查,这对我来说听起来像是一个笨拙而混乱的解决方案。

更新

感谢您的回复。很抱歉没有尽快回复您,但是您给我留下了很多文档要阅读,而且我还得自己做一些其他的研究!

所以,如果我的理解是正确的,我的 git-svn 存储库包含原始 svn 存储库的所有提交,并且它知道 svn 存储库包含分支和标签,但在本地它没有提交的 SHA1 和分支名称标签之间的关联,我必须手动添加这些关联。

您的代码段是一个非常有用的起点,谢谢!

我还发现了克隆命令的神奇参数 --mirror,它还导入了遥控器,因此我不必触及 git-svn 存储库,但后来我创建了直接在克隆的 git 仓库上分支。

TL;DR:您需要为每个要作为分支的分支创建实际的分支名称。 Remote-tracking 克隆时名称不算数(好吧,通常)。这可以很便宜!请继续阅读详细说明。

这是从每个 refs/remotes/svn/* 名称创建本地分支的廉价方法:

git for-each-ref --format='%(refname)' refs/remotes/svn |
    while read name; do
        local=${name#refs/remotes/svn/}  # remove the icky part from the name
        [ "$local" == HEAD ] && continue
        git branch $local $name
    done

这(注意:未经测试,可能有一些小错误)将为那些具有相应本地分支名称的名称打印错误消息;想必你可以忽略它。

... So I realised that git is cloning only the checked out branches, not the remote ones ...

"remote branch" 这样的东西真的不存在。好吧,除非您以存在的方式定义 "remote branch"。这最终给我们留下了首先定义 "branch" 的问题:请参阅 What exactly do we mean by "branch"? 在注意这一点时——与日常对话相反——我喜欢确保使用 two-word短语 分支名称 指的是像 master 这样的名称,实际上已经缩短了:见下文。

Git 处理的是 commits,由 names 和其他提交发现。请参阅 Think Like (a) Git 以了解 reachability 和许多相关内容的正确定义,1 但一般的想法是 names—full refs/heads/masterrefs/remotes/svn/foo 之类的名称——每个名称都包含一次提交的哈希 ID。该提交会记住哪些提交恰好在它之前。那些提交——parent 提交——记住他们的前任提交,grandparents 记住他们的 前任,等等上。

git clone所做的是:

  1. 创建一个新的空目录(或使用您告诉它使用的目录);
  2. 在该目录中创建一个新的空存储库,git init;
  3. 添加一个 remote,它由一个简单的名称组成,如 origin 和一个 URL(以及一些配置——这可以跳到第 4 步, 或被视为步骤 3 的一部分);
  4. 做任何额外的必要配置;
  5. 运行git fetch;最后
  6. 运行 a git checkout 你提供的名字,或者其他 Git 提供的名字,或者——最坏的后备情况——尝试 git checkout master.

这里的第 5 步对您来说是最重要的一步,因为 git fetch 是所有主要操作的地方。

Why is it not cloning all the branches?

git fetch运行s时,它从otherGit获取一个listing,其中otherGit告诉它 all 个名字。另一个 Git 会说,例如,我有 refs/heads/master,那是提交 a123456...;我有 refs/remotes/svn/foo,即提交 b789abc... 等等。

你的 Git然后丢弃任何开始的名字refs/heads/refs/tags/。生成的名称列表是它们的 Git 的 分支名称 标签名称 。所有其他名称都属于其他类别。特别是,任何以 refs/remotes/ 开头的名称都是 remote-tracking 名称 ,2 因此它会被丢弃。

您的 Git 然后向他们的 Git 询问提交(通过哈希 ID)和使提交完整和有用所需的任何其他 object。您的 Git 还要求通过标签名称识别 objects,只要您使用标签即可——尽管根据 git fetch 选项,具体使用哪些标签会变得非常复杂。

一旦你的 Git 有提交 objects,和其他内部 objects if/as 需要,你的 Git 然后复制他们的 branch 名字——他们的 refs/heads/master 等等——到 你的 remote-tracking 名字。他们的 refs/heads/master 成为你的 refs/remotes/origin/master。他们的refs/heads/develop(如果存在的话)成为你的refs/remotes/origin/develop

所有这些都发生在 git fetch 步骤(第 5 步)期间。 --single-branch--no-single-branch 等选项会影响匹配的分支名称,但不会影响从分支名称到 remote-tracking 名称的转换。 --mirror 选项 确实 影响转换,完全消除它,但也有一个 sometimes-unwanted 暗示 --bare 的副作用。

最后一步,即第 6 步中的 git checkout,有一个非常大的副作用。你刚刚创建的新克隆有 no 分支名称。3 所以 git checkout master 或任何其他名称显然注定要失败,对吧?但它不会失败。相反,Git 使用了一个聪明的 (?) 技巧:当您要求检查一个不存在的分支名称时,Git 查看 remote-tracking names 看看是否有匹配的。 如果是这样,Git 将 创建 (本地)分支使用存储在相应 remote-tracking 名称中的提交哈希 ID 的名称。

所以这 创建 您要求的任何分支 — 或者在这种情况下,由于您没有指定一个,另一个 Git 告诉您的 Git 另一个 Git 推荐的分支名称。 (无论如何,这通常只是 master。)创建它的是第 6 步。

如果您在 origin 存储库中有标签,那么您在新克隆中也会有一些标签(介于零和全部之间)。你可以明确地k 为后面的标签,或者没有,后面有git fetch。您可以在克隆时明确要求 not 在您的新克隆中添加标签。您此时拥有的标签只是从其他存储库中的标签复制而来。这里的想法是,与分支名称不同,分支名称对于每个存储库都是完全私有的,标签名称将在所有存储库之间共享,通过 repository-joining 传播,几乎就像某种病毒。4

由于您的源存储库大部分只有 remote-tracking 个名称,而不是分支,因此您的克隆(无论是否浅)都忽略了那些仅可访问的名称 提交 来自那些名字。


1这与 SVN 有很大不同,SVN 有一个中央服务器,可以简单地按顺序对每个修订进行编号。 Git 字面上 不能 依赖于顺序编号,因为可能有单独的克隆 sequentially-but-parallel-ly (为此处的 non-word 道歉)获取 不同 提交。也就是说,假设克隆 A 和 B 相同并且每个都有 500 次提交。然后,在克隆 A 中工作的 Alice 创建了提交 #501。同时,在克隆 B 中工作的 Bob 创建了提交 #501。这两个提交是不同的——可能在不同的分支上——它们都是#501。序号在这里不起作用。

2Git 称其为 remote-tracking 分支名称 。我曾经使用过这个短语,但我现在认为这里的 branch 这个词误导多于有用。您可以随心所欲地命名它:请记住它不是 分支 名称,因为它们实际上以 refs/heads/.

开头

注意:Git通常在打印名称时将此处的refs/heads/refs/tags/refs/remotes/部分去掉,假设输出仍然是够清楚了。有时 Git 只会去掉 refs/:尝试 git branch -r,然后尝试 git branch -a。 (为什么这些不同?这是一个谜。)

3如果您使用 --mirror,您的新克隆具有所有分支名称,但是 git clone skips 第 6 步。您的新克隆是裸机,因此没有 work-tree,并且无法使用 git checkout

4这也是提交传播的方式。假设您连续提交了 W、X 和 Y,而它们没有。你连接到他们的 Git 作为 push 操作,你给他们所有这三个提交并要求他们设置他们的名字之一来记住提交 Y,它会记住 X,它会记住 W,它会记住他们已经拥有的提交。

或者:他们有这些提交而你没有。你连接到他们的 Git 作为 fetch 操作,他们给你所有三个,你的 Git 设置你的 origin/whatever 现在记住提交 Y

基本上,您有两个 Git 存储库可以配对。一个发送,另一个接收。接收者得到了接收者要求发送者发送的所有新东西,即使接收者最终并不是真的想要它:在这一点上,接收者可以拒绝更新一些的请求name 记住提交链中的 last 提交。接收方因此保留其旧名称及其旧哈希 ID,或者没有名称(也没有哈希 ID)。

一个提交或其他 Git object 其散列 ID 无法找到它最终被 garbage-collected 丢弃。对于裸存储库,这往往会更快,并且自 Git 2.11 以来,服务器 "receive commits and other Git objects" 进程首先将它们粘在隔离区,然后再决定它们是好的并接受它们,或者决定它们'他们很糟糕并拒绝他们。被接受的然后从隔离区迁移到真正的存储库数据库,被拒绝的被迅速扔掉。 2.11 之前收到的 objects 立即进入,暂时使服务器膨胀,例如拒绝大文件(想想 GitHub 的 100MB 文件大小限制)。

浅克隆修改(部分)这些规则:对于浅克隆,接收方 Git 有一个充满哈希 ID 的特殊文件。它缺少那些实际的提交,但是 假装 它有它们,所以当发件人询问 "do you have commit X" 答案是 "yes",这样发件人就永远不会发送提交 X.