开始之前
在我开始回答之前,请注意,如果你想为每个远程跟踪名称创建本地分支名称,你可以直接创建该本地分支名称,而无需使用 git checkout
:
git branch -t develop origin/develop
git branch -t feature/X origin/feature/X
git branch -t foo origin/foo
等等,这是
git checkout
的一部分,它非常快,因为创建新的分支名称只需要编写一个文件。
(如果您愿意,您可以使用此技术并停在这里,但本回答的其余部分应该非常有用。)
短答案是您不必检出(或创建新的)分支名称。 但是,要使用 Git(包括此特定的
git filter-branch
操作),您需要了解更多内容。
让我们从这里开始:此处的
--all
意味着
所有引用。 那么什么是“引用”呢?
嗯,任何
分支名称 都是一个引用。 但是,任何
标签名称 也是一个引用。 由
git stash
使用的特殊名称
refs/stash
是一个引用。 远程跟踪名称是引用。 笔记引用(来自
git notes
)是引用。 有关此及其他 Git 术语的详细信息,请参见
gitglossary(请注意,此特定条目位于
ref
而不是
reference
下)。
当您首次使用
git clone
克隆存储库时,您告诉自己的 Git:
在我给出的 URL 上,创建一个现有存储库的新、独立副本,以便我可以做自己的工作,然后根据需要共享或不共享。 但是,他们的存储库——无论“他们”在 URL 上是谁——都有自己的分支名称。 他们拥有自己的
master
,这并不总是与您的
master
相同。 因此,您的 Git 会将它们的名称重命名:他们的
master
变成了您的
origin/master
等等。 这些
远程跟踪名称 是引用。
在
git clone
完成将所有提交复制到您的存储库,并将所有名称重命名为您的远程跟踪名称之后,
git clone
的最后一步是检出一个分支。 但是,您还没有任何分支。 这就是
git checkout
做的一个特殊技巧:如果您要求 Git 按名称检出一个不存在的分支,Git 将查看所有远程跟踪名称。 如果其中一个匹配,Git 将创建一个本地分支名称——指向此远程跟踪名称所指向的
相同提交的新引用。
因此,您的存储库具有一系列提交,所有这些提交以反向方式链接在一起:
first <--next ... <--almost-last <--last
(如果它们全都是线性的话,但它们几乎从来不会是这样)我们可以将其绘制为:
A--B--...--H--I
每个大写字母代表一个提交。一组带有一些“分支特性”(branchiness?)的提交可能如下所示:
C--D
/
A--B
\
E--F--G
如果存在合并提交,这些提交会向后指向两个先前的提交而不是一个,那么情况将变得更加复杂。
在这里我们最关心的名称——分支名称和远程跟踪名称,尤其作为Git查找最后一次提交的一种方式:
...--H--I <-- origin/master
名称origin/master
被认为是指向提交I
的。当你的Git创建了自己的master
后,你的master
也会指向I
:
...--H--I <-- master, origin/master
如果您在
master
分支上创建了新的提交,会发生以下情况:
...--H--I <-- origin/master
\
J <-- master
Git会为新提交生成一个新的ID,这是一些看似随机的丑陋哈希ID,但在这里我们只称其为J
,然后将您的名称master
更改为指向此新提交。
如果您运行git fetch
并从origin
获取新提交,并且它们已更新他们的主分支,则现在会出现以下情况:
...--H--I--K <-- origin/master
\
J <-- master
现在你的master
和origin/master
已经分叉。
这些名称,master
和origin/master
,有一个重要的作用,使它们的提交可达。也就是说,通过从每个名称开始的箭头,Git可以找到提交J
和K
。然后,使用向后的箭头——实际上是提交的父提交哈希ID——从J
到I
或从K
到I
,Git可以找到提交I
。使用I
本身的向后箭头,Git可以找到H
,以此类推,一直回溯到第一个提交,操作停止。
所有不可访问的提交——那些从所有这些起始(结束?)点开始并向后行走时未找到的提交——将在某个时候被删除,因此它们实际上不存在。对于大多数遍历图形的Git命令来说,这也是如此。(有一些特殊的恢复技巧可以让您在30天内重新获取已删除的提交,但filter-branch不支持这些技巧。)
filter-branch对所有这些的影响
git filter-branch
的工作是复制提交。它遍历图形,使用您提供的起始(结束?)点找到所有可访问的提交。它将它们的哈希ID保存在一个临时文件中。然后,向相反的方向——即向前而不是Git通常的向后——提取每个提交。也就是说,它检出它,以便该快照中的所有文件都可用。然后filter-branch应用过滤器,然后从生成的文件中创建一个新的提交。因此,如果您的过滤器进行了简单的更改,则结果是原始图形的副本:
A--B--C------G--H <-- master, origin/master
\ /
D--E--F
变成:
A'-B'-C'-----G'-H' <-- master, origin/master
\ /
D'-E'-F'
原始提交内容会发生什么变化呢?实际上,它们仍然存在:filter-branch 会用 refs/original/
作为前缀,对找到的原始提交进行重命名,但并不会删除它们的内部全名。
A--B--C------G--H <-- refs/original/refs/heads/master, refs/original/refs/remotes/origin/master
\ /
D--E--F
一个使用filter-branch的原因是这个过程非常缓慢。将每个文件提取到临时目录需要很长时间。因此,一些过滤器可以在不提取文件的情况下工作,速度快得多。
另一个原因是有时我们不想复制每个提交,只想复制符合某些条件的一些提交。这就是--subdirectory-filter的情况:它仅在更改与所涉及子目录相关的文件(相对于其父提交)时复制提交。因此,在某些情况下,它可以跳过提取许多提交的步骤。当然,子目录过滤器还会在提取和重新提交时重命名文件,以删除子目录路径。结果是将较大的提交图复制到更新的、较小的提交图中:
A--B--C------G--H <-- master
\ /
D--E--F
可能会变成:
B'--G'--H' <-- master
\ /
E'
保留的
refs/original/refs/heads/master
仍将指向提交
H
,而重写的
refs/heads/master
将指向复制的提交
H'
。请注意,新图表中的第一个提交是
B'
,而不是
A'
,因为
A'
没有相关的子目录。
这里还有一个非常重要的问题:
filter-branch在完成所有提交复制后会更新哪些引用?答案在文档中:
引用如下:
该命令仅将命令行中提到的正数引用进行重写(例如,如果传递a..b,则只会重写b)。
由于您正在使用
--all
,因此这将重写所有
origin/*
远程跟踪名称。(在此处,
--all
计为每个引用的正数提及。关于标签还有一些额外的技巧:如果要重写标签,请添加
--tag-name-filter cat
作为过滤器。)
总结:
在filter-branch操作之后,您拥有一系列指向原始(经过过滤之前)提交的
refs/original/*
名称,它们已从其原始全名重新命名。您拥有一系列新的更新引用,包括所有分支名称(
refs/heads/*
)和远程跟踪名称(
refs/remotes/*
),它们都指向最后一个被复制的提交。
新仓库将比原始仓库更大,因为它包含原始内容以及复制的提交。请参阅
git filter-branch文档中“缩小存储库的清单”部分,但要注意,如果您使用
git clone
复制过滤后的存储库,则只会复制您的
分支名称,而不是您的
远程跟踪名称,所以此时,如果您尚未为每个远程跟踪名称创建分支,则应立即执行该操作。
另一种方法是,在删除所有refs/original/
名称空间后,将复制的仓库保留在原地。 然后,您可以git checkout develop
,根据(过滤后的)refs/remotes/origin/develop
创建自己的refs/heads/develop
,以此类推。您所做的只是创建新名称 - 提交本身才是Git真正关心的内容,并且它们由重写的远程跟踪名称引用 - 然后检出该特定提交,以便它位于您的索引和工作目录中。(我们在开头展示的git branch -t
命令创建了没有将提交复制到索引和工作目录的名称。)