abu*_*kay 0 git rebase git-rebase
按照本指南,我通过删除历史上的一些重文件来缩小我们的项目存储库。这意味着 git 历史已经改变。但现在的问题是,我的其他团队成员如何在不丢失他们对当前不在远程的分支上所做的更改的情况下获得我们的 repo 的新缩小版本,并且也不会推回已删除的历史记录。
作者建议 clone 或 rebase:
拥有存储库本地克隆的任何其他人都需要使用 git rebase,或者创建一个新的克隆...
新的克隆意味着放弃任何团队成员在本地所做的所有更改。因此,rebase 似乎是更好的选择。但是我们该怎么做呢?
我在想这样的事情:(假设 master 是新功能分支的基础分支,开发人员机器上的本地分支有新工作,并且 master 受到历史重写的影响):
$ git checkout master
$ git fetch origin
$ git pull --rebase
$ git checkout new-feature
$ git rebase master
Run Code Online (Sandbox Code Playgroud)
确认一切正常然后
$ git push origin
Run Code Online (Sandbox Code Playgroud)
该指南是OK(尽管也有一些技巧,以改善的事情,并且存储库的清洗会走多快用“BFG” -见其他StackOverflow的帖子,包括那些由BFG的作者)。而且,这部分是正确的:
拥有存储库本地克隆的任何其他人都需要使用 git rebase,或者创建一个新的克隆...
不幸的是,您建议的 rebase 步骤是错误的。实际所需的步骤有点棘手。只有当你理解 Git 的散列、提交图和分发存储库背后的想法时,它们才会变得清晰;你做了什么git filter-branch;以及您可以用git rebase. 还有另一种方法,使用git format-patch, 来git rebase完全避免——但你需要知道多少才能使用它。
(使用 rebase 时,我们可以使用--fork-point,至少在大多数情况下。见下文。)
What git filter-branchdo 在某些方面与 what git rebasedo相似:它们都复制 commits。两者之间的最大区别在于哪些提交被复制以及如何执行复制。
接下来您需要了解关于 Git 提交的一件事——您实际上已经知道这一点,但您需要更好地理解它——每个提交都由其哈希 ID唯一标识。这些是8f60064...Git 不断向你展示的那些丑陋的东西。这些ID是多么的Git发现的每一个承诺,但有关于他们的一个关键的事实,这一点,他们通过计算的加密校验产生内容的承诺。提交的内容取决于许多其他东西,包括所谓的树——源代码快照;每当您删除一个大但不必要的文件时,您都会更改此树 - 以及提交的先前或父提交。
同样,这个加密哈希 ID 严重依赖于提交的内容。但同时,这完全是确定性的:如果您给 Git相同的内容,您将获得相同的哈希 ID。这实际上适用于所有 Git 的对象,而不仅仅是提交。所有四种——文件(称为blob)、树、带注释的标签和提交——都使用相同的散列技术,对一个位对位相同的文件或树或标签或提交进行散列会产生与上次相同的散列。这意味着,例如,保存一个文件的特定版本——不管它有多大——在50 次提交中所占用的空间与在一次提交中保存它所占用的空间完全一样。犯罪。但是,一旦您更改了文件,您就会拥有一个新版本,不再是一点一点相同。保存它会产生一个新的和不同的哈希,它保存文件的新副本。散列只是让 Git在其对象数据库中找到对象:您必须知道散列才能获得实际内容。随时从内容转到散列很容易:只需散列内容即可。而且,如果您知道散列,就很容易获得存储的内容:如果散列在数据库中(作为键),它们就在数据库中(作为值)。
因为内容决定了哈希 ID,所以我们永远无法更改 任何 Git 对象的任何内容。如果我们要更改一点,然后再次散列,我们将得到一个新的、不同的散列 ID。这意味着我们——和 Git——实际上永远无法改变任何东西。我们只能把它复制到一个新的、略有不同的东西上。我们制作副本,对其进行散列,然后查看散列是否在数据库中。如果没有,我们存储副本与新的哈希,现在它是在数据库中。
当我们(或 Git)从整体上查看存储库时,我们可以绘制该存储库中所有提交的图表。有很多不同的绘制图形的方法,但是对于 StackOverflow 的帖子,我用左边的“较早”提交和右边的“较晚”提交来绘制它们。每个后来的提交都“指向”其父之前的提交。使用简单的线性链——它构成了存储库的大部分——这给了我们看起来像这样的东西:
A <- B <- C <- D <-- master
Run Code Online (Sandbox Code Playgroud)
请注意分支名称如何master“指向”最近的提交,D(我使用单个字母而不是丑陋的 40 个字符的大哈希,直到我用完单个字母:-))。Git 将此称为分支的尖端。这个最新的提交“指向”它的父提交C;C点回B,B点回A。由于A是有史以来第一次提交,因此没有更早的提交指向它,因此它不会指向任何地方。CommitA是一个根提交,它停止所有的遍历动作。
通常我们不需要内部箭头,所以我把它们画成:
A--B--C--D <-- master
\
E--F <-- branch
Run Code Online (Sandbox Code Playgroud)
这更紧凑,使用线条而不是箭头。但请注意,此处,E具有B作为其父级:E“指向回”到B. 虽然没有箭头的头,你可以知道哪些承诺是父,而这是孩子,来自全国各地的连接线的相对位置:孩子是父母的权利,以及父母的留守儿童。
当 Git开始处理存储库中的提交时——比如git log甚至git merge——它从提示提交开始,分支名称指向的那些。然后它使用内部指针来查找每个以前的提交。也就是说,名称,master和branch只是开始。一旦我们开始,Git 使用内部的“连接箭头”来查找所有其余的提交。
尽管我在这里使用了单个字母,但每个提交都有一个完整的加密哈希 ID,并且每个提交都存储了前一个提交的 ID。那怎么D 能“点背”来C,例如。这些存储的 ID 参与加密哈希,这意味着C的 ID会影响 D的 ID。等效地,我们可以说D的 ID取决于 C。还要注意,那个DID取决于与 commit 一起保存的工作树快照D。如果C或D是首先引入大文件的地方,然后我们去删除那个大文件,那么......现在让我们看看它是如何filter-branch工作的。
在基本级别上,在任何优化之前,git filter-branch复制的是存储库中的每个提交。(更准确地说,每个提交都可以从指定的分支或其他引用访问;使用--all,这实际上意味着一切,前提是我们做出一些非常安全的假设,或者像您的指南中那样从一个新的克隆开始。)
当过滤器分支工作时,它复制每个提交的方式是首先将它提取到它的所有组成部分。然后它运行你的过滤器。这些过滤器可以改变事物(毕竟这是过滤器的重点)。
无论他们做了什么更改,Git 现在都必须重新计算哈希 ID。如果事实证明过滤器实际上没有改变任何东西,新的散列将与旧的散列相同——但我在这里有点领先。对于文件来说完全正确:如果您不更改文件,它将保留其旧哈希。如果您更改文件,新内容将获得新的哈希值。对于存储的树(整个文件集合的快照)也是如此。但假设我们正在与:
A--B--C--D <-- master
\
E--F <-- branch
Run Code Online (Sandbox Code Playgroud)
Git 要做的是以某种适当的顺序过滤所有提交。它会先做A,然后B。然后它有一个选择:它可以做E然后F然后回去做C和D,或者它可以做C和D,然后回去做E然后F。(在图论术语中,Git 必须对图进行拓扑排序。)我们不需要担心细节——Git 会处理这些——但我们确实需要观察 Git 复制每个提交时会发生什么。
为了简单和具体起见,我们假设副本按字母顺序排列(A、B、C、D、E、F)。假设我们的过滤器是“删除大文件”。
现在,假设大文件不在commit 中A。Git 提取A并应用过滤器。这会尝试删除大文件 - 但它不存在!所以实际上什么都没有改变。Git 现在从剩下的内容进行提交,并且新的提交与原始的A. 所以它得到相同的哈希 ID:A is 的副本A。
Git 现在继续提交B并重复这个过程。如果B不改变,它的“副本”仍然是B.
Git 继续提交C。这次提交确实有大文件——所以我们的过滤器将其删除,然后 Git 进行新的提交。此提交不再是逐位相同的,因此它会获得一个新的哈希值,并作为新的不同提交存储在数据库中。因为它是一个复制的C,我们称之为承诺C':
C'
/
A--B--C--D
\
E--F
Run Code Online (Sandbox Code Playgroud)
现在 Git 继续提交D。我们将复制 commit D。副本会与原件逐位相同吗?好吧,如果我们必须删除一个文件,当然不会。但是——假设提交的人C意识到了他们的错误,并删除了大文件。现在,该副本可能逐位相同。但这将是一个错误,因为 commitD指向 commit C。 我们需要一个指向回的提交,不是指向C,而是指向C'! 因此,无论 commitD是否包含大文件,Git 都会进行不同的新提交。我们的新副本D'不仅省略了大文件——如果它在那里——而且还指向我们复制的C':
C'-D'
/
A--B--C--D
\
E--F
Run Code Online (Sandbox Code Playgroud)
Git 现在继续复制E和F. 如果他们没有大文件,他们的副本只是他们的原件。如果E 确实有大文件,它的副本是一个新的 commit E',并且强制 Git 也复制F到F'。如果只有F大文件,Git 可以重用原始文件,E但需要一个新副本F'。
这归结为每一个下游提交的变化,也会发生变化(“下游”在这里的意思是“是一个孩子,或孙子,或其他进一步的后代”)。一旦我们复制了一个提交,这个变化就会在图表的其余部分冒泡。
如果我们必须修改B,下游的每个提交B也会被复制。如果我们必须先修改A,那么每个提交都会被复制,给出:
A--B--C--D
\
E--F
A'-B'-C'-D'
\
E'-F'
Run Code Online (Sandbox Code Playgroud)
(这是一个有效的提交图!它由两个所谓的不相交的子图组成。Git 处理这种事情没有问题。)
最后一件事git filter-branch是移动所有分支名称(如果我们有一个--tag-name-filter,也移动相应的标签名称)。它将这些分支移动到复制的提示提交。如果我们只复制Cand D,这是我们的最终图,标签重新指向:
C'-D' <-- master
/
A--B--C--D [abandoned]
\
E--F <-- branch
Run Code Online (Sandbox Code Playgroud)
虽然提交C并D实际上仍在存储库中,但它们现在无法访问。他们没有名字master可以找到他们。
为了实际缩小存储库,磁盘空间,我们现在必须说服 Git 丢弃原始C和D(通过它们的哈希 ID)。Git 通常最终会自己做这件事,除了:
git filter-branch将原始名称保留为refs/original.因此,我们必须删除这些名称(如您的指南中所示),然后使用更多的“maintenance-y”Git 命令使到期立即发生,而不是最终发生。
git rebase作用您现在了解了git filter-branch工作原理,复制每个提交,有时最终会以逐位相同的副本结束,因此实际上与原始副本相同,但有时必须更改已更改提交的“下游”每个提交。现在您确实明白了这一点,这git rebase似乎很简单。
rebase 命令,如 filter-branch,复制提交。但是,至少通常情况下,它首先将每个提交变成一个补丁——或者更准确地说,一个带有一些历史的补丁,或者一个git cherry-pick(这些都有微妙的不同,我们不需要在这里讨论) )。
回顾我们绘制的提交图。每个提交都有一些父级。大多数提交只有一个父级。少数(至少一个)可以没有父级,而有些(Git 称之为 *merge commits)有两个或更多父级。
对于只有一个父级的任何提交,这是大多数,我们可以运行将新的子提交与其旧的父级git diff进行比较,以查看发生了什么变化。它的输出git diff是一组指令:“将父提交更改为子提交,删除这些行并添加这些其他行。” 这是一个补丁。由于它现在是一个补丁——一组更改,而不是一个快照——提交的这个补丁版本可以应用于不同的源快照。
这不适用于合并提交,因为它们至少有两个父项。(我没有在上面绘制任何合并提交。)所以git rebase通常只是完全跳过它们。它也不适用于 root 提交;并且通常你也不会对它们进行重新设置(而且它首先没有多大意义)。
通过将每个 commit-to-rebase 变成一个补丁,git rebase可以将多个提交复制到图中的新位置。例如,给定:
A--B--C--D <-- master
\
E--F <-- branch
Run Code Online (Sandbox Code Playgroud)
我们可以复制,E--F以便新的副本出现在 D:
A--B--C--D <-- master
\
E'-F' <-- branch
Run Code Online (Sandbox Code Playgroud)
为此,我们告诉git rebase要复制哪些提交:
git checkout branch # i.e., end the copy with the tip of branch
Run Code Online (Sandbox Code Playgroud)
并且承诺不复制:
git rebase master # i.e., *don't* copy commits that are on master
Run Code Online (Sandbox Code Playgroud)
以及在哪里放置副本:
git rebase master # i.e., put the copies after the tip of master
Run Code Online (Sandbox Code Playgroud)
请注意,<upstream>to的参数在这里git rebase做了两件事,即指定不复制的内容和放置副本的位置。
这适用于大多数(但不是全部)常规 rebase。它对我们不起作用,有两个原因。--onto正如我们将看到的那样,使用 很容易处理。另一个比较棘手。
大多数 Git 命令通过哈希 ID 计算出所有内容。这也是正确的git rebase:它知道哪些命令是复制的候选者,哪些不是,通过它们的哈希 ID。但是我们git filter-branch使用不同的哈希 ID运行并复制提交到新的提交。现在,rebase 确实有一些额外的智能东西,以解决一些复制提交的情况,但它们对我们帮助不大,我们稍后会看到。
现在,我们的问题不同了。其他人,而不是我们-RANgit filter-branch一些中央存储库,并把我们的A-B-C-D进入A-B-C'-D'。我们也可能会或可能不会E-F脱落B。但是——这是棘手的部分——我们有自己的存储库,它与集中式存储库分开,它有我们自己的提交G-H:
G--H <-- feature
/
A--B--C--D <-- master, origin/master
\
E--F <-- origin/branch
Run Code Online (Sandbox Code Playgroud)
一些小丑 :-) 已经完成并filter-branch在中央存储库上运行并替换C-D为C'-D'. 我们现在在那个中央存储库上运行git fetch——不要使用git pull——来获取他们的新提交。这给了我们他们的新提交,我们保留自己的提交。我们现在有:
G--H <-- feature
/
A--B--C--D <-- master
|\
| \
| C'-D' <-- origin/master
\
E--F <-- origin/branch
Run Code Online (Sandbox Code Playgroud)
请注意,我们自己master的没有被触及。我们自己的原件C和D仍然在我们自己的存储库中。他们的副本C'和D',现在被添加到我们的收藏中,我们origin/master已经开始记住他们的新master. 我们没有自己的branch,只有origin/branch,但这一次没有改变。
我们现在需要做的是复制我们的G-H提交。它们位于尖端名为 的分支上feature。但是我们的C和D原件也在这个分支上。他们的名字master指向他们。
你建议我们git rebase在我们的分支上运行master。(这就是git pull --rebase它的作用:它首先运行git fetch,然后运行git rebase而不是运行git merge)。让我们看看如果我们这样做会发生什么。
这是我们的起始图,减去origin/branch我们不关心的:
G--H <-- feature
/
A--B--C--D <-- master
\
\
C'-D' <-- origin/master
Run Code Online (Sandbox Code Playgroud)
我们运行git checkout master; git rebase origin/master,这或多或少是你建议的git pull。我们说我们想要复制master基于当前分支的提交,来自git checkout——同时排除在 上的提交origin/master。但那些是提交A和B. 因此,我们将复制C,也许D,然后将我们的副本放在origin/master:
G--H <-- feature
/
A--B--C--D
\
\
C'-D' <-- origin/master
\
C'' <-- master
Run Code Online (Sandbox Code Playgroud)
让我们谈谈 的“智能”部分git rebase:它知道不能盲目地相信提交哈希 ID。它所做的也是将一堆提交转换为git patch-idID。没有进入很多的细节,这可以让我们git rebase避免复制D。不过,它绝对不起作用C。
记住从哪里来C':它是C减去大文件。删除大文件打破了补丁 ID 的智能:Git 看 B-vs-C 和 B-vs-C',它们看起来不同。因此 rebase 决定它必须C再次复制到C''. 这会重新添加大文件.
是否D被复制到D''取决于 C-vs-D 中的内容以及现在 C'-vs-D' 中的内容。也许它确实被复制了,也许它没有被复制,但无论如何,损害已经造成:大文件又回来了!就在你以为它消失的时候!
我们想要的是复制G-H. 这就是git rebase's--onto有用的地方——但我们还需要更多。
请记住,<upstream>参数 togit rebase指定了不复制的内容和放置副本的位置。使用--onto,我们可以告诉 rebase将副本放在哪里。
我们知道把副本放在哪里:他们应该去追origin/master。所以我们将添加--onto origin/master. 副本现在将在 commit 之后进行D'。
至于什么不能复制:嗯,这其实很简单,只要我们还没有触及我们自己的master。 我们想复制feature不在我们的 master. 也就是说,我们希望D更早地排除提交和所有内容。所以,这就是我们应该为<upstream>.
这给了我们最终的git rebase命令序列:
git checkout feature
git rebase --onto origin/master master
Run Code Online (Sandbox Code Playgroud)
上面git checkout写着“继续工作feature,即提交结束于H”。第二部分,实际的 rebase 命令,说“省略我们 上的提交master,而将副本放在后面origin/master”。
这是结果:
G--H [abandoned]
/
A--B--C--D <-- master
\
\
C'-D' <-- origin/master
\
G'-H' <-- feature
Run Code Online (Sandbox Code Playgroud)
现在还有一件事要做,一旦我们复制了我们关心的所有提交。我们现在必须重置我们master的匹配origin/master。为此,我们将使用git reset --hard:
git checkout master
git reset --hard origin/master
Run Code Online (Sandbox Code Playgroud)
请注意,我们仅在使用保存的重新定位完成后才执行此操作master,以确保我们不会复制 commit D。最后的结果reset是:
G--H [abandoned]
/
A--B--C--D [abandoned]
\
\
C'-D' <-- master, origin/master
\
G'-H' <-- feature
Run Code Online (Sandbox Code Playgroud)
这就是我们想要的。
feature呢?当我们在我们git rebase --onto之后做我们的git fetch和时,我们得到了这张图git checkout:
G--H <-- feature
/
A--B--C--D <-- master
\
\
C'-D' <-- origin/master
Run Code Online (Sandbox Code Playgroud)
但是如果我们已经提交G并H直接提交了master呢?然后我们会有这个:
A--B--C--D--G--H <-- master
\
\
C'-D' <-- origin/master
Run Code Online (Sandbox Code Playgroud)
如果我们处于这种情况下,我们的工作就会困难得多。我们必须找出哪些提交被复制,也就是说,这是提交C和D,哪些是C'和D'。
如果我们坐下来画这张图,就很明显了。但是现实世界的 Git 图非常混乱。(这就是我们首先使用分支名称的原因:计算机可以为我们跟踪混乱情况。)
事实证明,Git 的引用日志是我们的救星。当我们跑去git fetch捡起时C'-D',这会将我们origin/master从指向D、指向D'。reflog 条目 ,origin/master@{1}仍然指向D:
G--H <-- master, feature
/
A--B--C--D <-- reflog: origin/master@{1}
\
\
C'-D' <-- origin/master
Run Code Online (Sandbox Code Playgroud)
这意味着我们可以feature使用以下命令修复我们的分支:
git checkout feature
git rebase --onto origin/master origin/master@{1}
Run Code Online (Sandbox Code Playgroud)
(尽管取决于您的外壳,您可能需要将最后一个参数括起来:外壳可能会尝试吃掉该{1}部分并对其进行处理)。在 Git 2.0 及更高版本中,git rebase使用内置了这种聪明--fork-point,因此您可以使用:
git rebase --fork-point origin/master
Run Code Online (Sandbox Code Playgroud)
这适用于许多情况,并且通常是在上游重写(无论重写是git filter-branch还是git rebase)后变基的技巧。
在任何情况下,无论您如何变基,在推送之前仔细检查新的“传出”提交都是值得的。要检查这些提交:
git fetch origin
git log -p origin/master..feature
Run Code Online (Sandbox Code Playgroud)
(假设您feature最终会被推送到master)。
git format-patch我上面提到你可以使用git format-patch代替git rebase. 这对某些人来说可能更舒服,因为这让您有机会检查每个补丁,并且您可以将您的工作提取为一堆补丁,然后重新克隆原始存储库(而不是更新现有但现在已经过时的克隆,从过滤后的)。
我们知道这git rebase会将每个要重新定位的提交变成一个补丁。我们只能自己做。将一些提交转换为补丁的命令是git format-patch.
假设feature我们master在我们的存储库中有我们的分支,基于我们的, 。我们知道有人已经过滤了中央存储库,我们还不想获得过滤后的存储库(或者我们已经在其他地方单独克隆了它)。我们现在想要的是生成每个feature提交,那些在我们之后的提交master,作为一个补丁,所以我们只需运行:
git format-patch --stdout master..feature > /tmp/as-a-patch
Run Code Online (Sandbox Code Playgroud)
现在我们可以查看文件以查看我们有哪些提交以及它们做了什么。这基本上相当于git show在每次提交时运行。
一旦我们检查了补丁并确定它们是正确的,我们就可以转到新的、过滤的存储库的新克隆并创建一个新的功能分支:
git clone <url> # clone the filtered repo
cd new-clone # switch to the new clone
git checkout -b feature master # make a new feature branch
git am /tmp/as-a-patch # apply the patches
Run Code Online (Sandbox Code Playgroud)
这东西是为电子邮件补丁从一个帐户到另一个,故名git am:一个pply ê邮件。
由于我们从不将旧的、预先过滤的克隆与新的、过滤后的克隆混合,并且我们仔细检查了我们的“电子邮件”补丁文件,因此不存在意外重新引入大文件的危险。
| 归档时间: |
|
| 查看次数: |
1469 次 |
| 最近记录: |