首页 > 解决方案 > git rebase 一个不包含新提交的分支,不需要 - 推送所需的力?

问题描述

对不起,如果这个问题已经被 awnsered 但我找不到 awnser。

我来这里是因为工作中的一次非常痛苦的经历:我将 master 重新定位到错误的分支,然后执行了一个git push.. 这似乎把事情搞砸了。现在我试图了解出了什么问题,以及为什么即使我没有使用--force标志,我的更改也会被推送。

关于我们的 git 分支策略的一些信息:

我们有不同的旧“版本”分支,我们只实现错误修复(分支 v1.0、分支 v2.0 等),因为我们需要支持这些旧版本。然后是 master 分支,我们实际上在该分支上实现了新功能。

每当在软件的旧版本 fe 分支 v1.0 中发现错误时,我们都会从该版本分支出来,为错误修复创建一个功能分支。然后我们在 v1.0 分支之上重新调整这个特性分支中的更改,然后在 v1.0 上进行快进合并。然后通过合并提交,我们将 v1.0 合并到 v2.0 中,最后合并到 master 中,以便在所有较新版本的产品中也修复了该错误。因此,在旧版本分支(fe 分支 v1.0)上进行错误修复/更改的流程如下所示:

  1. 从 v1.0 分支出来(创建一个特性分支)
  2. 做一些改变
  3. git rebase origin/v1.0将功能分支中的更改移动到最新的 v1.0 分支之上
  4. git push -f origin feature_branch
  5. 快进合并功能分支到 v1.0
  6. 通过合并提交将 v1.0 合并到 v2.0
  7. 通过合并提交将 v2.0 合并到 master
  8. 现在,在 v1.0 上所做的所有更改(仅限错误修复)也适用于较新版本的软件

例如,对于软件 v1.0 的错误修复,合并过程如下:

FB -> v1.0 -> v2.0 -> master

简而言之:v1.0 是该产品的最旧版本,v2.0 将包含 v1.0 中的所有提交以及 v2.0 版中所做的附加功能提交,master 将包含 v2.0 中的所有提交以及其他功能提交对于产品的新版本。

我做错了什么: 正如我所说,当将错误修复合并回父分支时,我需要首先在父分支之上重新设置更改,因为与此同时父分支上可能还有其他更改,然后快进合并回父级。

我正在开发一个应该只进入 master 分支的功能,所以很自然地我尝试将 master 重新定位到我的分支中,以使我的更改位于所有其他更改之上。但是我没有将 master 分支重新定位到我的功能分支,而是在 v1.0 分支上并将 master 重新定位到 v1.0 分支(所以,不是我的功能分支)。这导致 master 被重新定位到 v1.0 分支. 更糟糕的是,在没有彻底检查的情况下,我也推送了 v1.0 分支。 结果:v1.0 分支现在看起来和 master 完全一样……不好。

现在我的问题是:我只是执行了一个错误的git push,我没有--force pushv1.0 分支。我对变基的理解是,当您进行变基时,您会重写分支的历史记录,因此您需要使用 git push —force,否则远程将不会接受您的更改。在这种情况下是否没有发生历史重写,因为 master 已经包含 v1.0 分支的所有提交以及 v1.0 分支不包含的一堆额外提交?

我真的很想正确理解这一点,因为如果我需要强制推动,更多的警钟会开始为我敲响,我想这不会发生。

标签: gitgit-mergerebasegit-rebase

解决方案


这有点长,因为您想真正了解正在发生的事情,所以我将提供更多信息,而不仅仅是直接回答您的问题。但是,如果您对此一无所知: 在 push 之前验证您的本地状态。(紧随其后的是:对武力推动更加持怀疑态度。)


人们习惯认为“rebase”==“需要强制推送”,两者在某种程度上是相关的;但不仅仅是变基的行为产生了强制推动的需要。这是从分支的历史记录中删除提交的行为(比如 branchX),然后只有 branchX 需要被强制推送

因此,考虑到这一点,让我们来看看你的工作流程——首先是它打算工作的时候,然后是这个错误发生的时候。首先,您的回购可能看起来像

... O <--(origin/v1.0)(v1.0)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

这里的...意思是“我们并不真正关心的一些历史”,x意思是“一个提交,但不是我将在本次讨论中具体引用名称的一个”,M意思是“一个合并提交,但不是我要去的一个在此讨论中特别提到名称”。其他字母表示我可能按名称引用的提交。如果我可以按名称引用合并,我会称它为M1. 然后/, \, 和--显示提交之间的父子关系(较新的提交在右侧),括号中的名称是一个 ref(例如一个分支),带有一个箭头,显示 ref 的当前提交。

除了本地分支之外,我还展示了远程跟踪参考 - 即您的 repo 对远程分支在哪里的理解。

所以...

预期行为

1) 从 v1.0 分支出来

... O <--(origin/v1.0)(v1.0)(feature_branch)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

在这里,我们刚刚创建了一个新的 ref,它指向与版本分支相同的提交。

2)做一些改变

      A <--(feature_branch)
     /
... O <--(origin/v1.0)(v1.0)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

3)git rebase origin/v1.0

这一步似乎有些悲观。您是否对旧版本的产品进行频繁、并发的更改?如果没有,我会考虑仅将其作为异常处理步骤,以应对实际上对v1.0. 上图将保持不变,但如果我们假设有中间变化

      A <--(feature_branch)
     /
    | B <--(origin/v1.0)
    |/
... O <--(v1.0)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

那么这一步会给你

        A' <--(feature_branch)
       /
      B <--(origin/v1.0)
     /
    | A
    |/
... O <--(v1.0)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

请注意,feature_branch“已移动”,因此A已从其历史记录中删除,而B新的提交(A'- 的补丁副本A)已添加到其历史记录中。

我仍然A在图片中显示,因为它仍然存在,尽管目前没有任何引用它。(但我很快会再次谈论它......)这强化了一个重要的观点,这经常被误解: rebase 没有“移动” A。它创建了一个新的提交,A',它不同于A. 这就是为什么我说它A已从feature_branch的历史中删除。

无论如何,所有其他裁判都保留了他们已经拥有的所有历史。

4)git push -f origin feature_branch

这有点令人困惑,因为您没有显示您之前已 push feature_branch。如果您没有,则-f不需要该标志 - 因为即使您从feature_branch的本地历史记录中删除了提交,远程也对此一无所知。

也就是说,完善我上面所说的内容 - 仅在推送您已从其历史记录中删除了作为该分支的远程历史记录的一部分的提交时才需要强制推送。

所以让我们假设在变基之前你已经pushfeature_branch了,图表看起来真的像

        A' <--(feature_branch)
       /
      B <--(origin/v1.0)
     /
    | A <--(origin/feature_branch)
    |/
... O <--(v1.0)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

(这是我保留A在图表中的真正原因。)现在你将无法在feature_branch没有-f标志的情况下推送,因为推送会A远程'feature_branch的历史记录中删除。

但是现在是提提意见的好时机……根据我对第 3 步的评论,您应该警惕使用强制推送作为正常步骤的工作流程。就像遥控器知道A并且feature_branch必须被告知历史已被编辑一样,如果任何其他开发人员已经fetched 或pulled feature-branch,那么 force-push 将使他们的 repo 处于损坏状态。他们将不得不恢复,特别是如果他们对 ; 进行了额外的更改feature-branch。如果他们做错了,它可能会撤消变基。

也就是说,后期push图片将是

        A' <--(feature_branch)(origin/feature_branch)
       /
      B <--(origin/v1.0)
     /
... O <--(v1.0)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

(这次我删除了A,因为我们已经不再担心它了。它仍然存在,仍然可以在 reflog 中访问,但最终gc会销毁它,除非你采取措施复活它。)

5) 快进合并feature_branchv1.0

大概你也意味着v1.0在快进之后推动。因为它是快进(即使对于遥控器),不需要强制推送;也就是说,远程曾经看到的每个提交v1.0仍然是v1.0.

... O -- B -- A' <--(v1.0)(origin/v1.0)(feature_branch)(origin/feature_branch)
     \
   .. M -- x <--(origin/v2.0)(v2.0)
            \
         ... M -- x <--(origin/master)(master)

5 和 6) 向前合并

这很简单,并且不需要强制推动。

... O ----- B ---- A' <--(v1.0)(origin/v1.0)(feature_branch)(origin/feature_branch)
     \              \
   .. M -- x ------- M <--(or-igin/v2.0)(v2.0)
            \         \
         ... M -- x -- M <--(origin/master)(master)

好吧。现在据我了解,当您从master. 在这一点上,我将为几个x提交添加不同的名称,并删除一些我们不会谈论的 refs

... O ----- B ---- A' <--(v1.0)(origin/v1.0)
     \              \
   .. M -- V ------- M <--(or-igin/v2.0)(v2.0)
            \         \
         ... M -- W -- M <--(origin/master)(master)

所以在第1步和第2步之后你有

... O ----- B ---- A' <--(v1.0)(origin/v1.0)
     \              \
   .. M -- V ------- M <--(or-igin/v2.0)(v2.0)
            \         \
         ... M -- W -- M <--(origin/master)(master)
                        \
                         C -- D <--(feature2)

但是随后您开始完成上述工作流程,就好像它是一个v1.0功能一样。所以对于第 3 步

git rebase origin/v1.0

如你所知,这将是一个麻烦。当前分支的历史中所有不在origni/v1.0历史中的,都被视为“需要复制”。

                     V' -- W' -- C' -- D' <--(feature)
                    /
... O ----- B ---- A' <--(v1.0)(origin/v1.0)
     \              \
   .. M -- V ------- M <--(or-igin/v2.0)(v2.0)
            \         \
         ... M -- W -- M <--(origin/master)(master)
                        \
                         C -- D

合并提交被忽略(默认行为rebase; 无论如何它们都不应该引入不同的更改,尽管冲突解决和/或“邪恶的合并”可以打破这个假设)。但是VW没有被忽视。与往常一样,请注意,V当前W分支之外的每个分支的历史记录保持不变。

与上述工作流程一样,您现在可以推送feature. 如上所述,如果您在变基之前曾经push编辑过feature,那么您现在必须强制推动它......但是您的典型工作流程已经欺骗了您无论如何都期望它,所以虽然它应该引发一个危险信号,但它不会.

无论哪种方式,您都可以看到它v1.0会愉快地快进feature(因为feature's 的历史无论如何都包含v1.0's 的所有历史),这意味着v1.0将不加力地推动。

所以这就是它出错的原因,但接下来该怎么办?

我的第一条建议是不要对随意的强制推动感到不舒服。乍一看,由于强制推送和历史重写(如rebase)在某种程度上是相关的,这听起来像是使用合并而不是变基的理由......但这并没有真正的帮助。如果您将功能编码在一个分支上master

... O ----- B ---- A' <--(v1.0)(origin/v1.0)
     \              \
   .. M -- V ------- M <--(or-igin/v2.0)(v2.0)
            \         \
         ... M -- W -- M <--(origin/master)(master)
                        \
                         C -- D <--(feature2)

但随后错误地认为你需要去v1.0,合并将同样默默地工作,结果同样错误。

                     -------------------------------- M <--(v1.0)
                    /                                /
... O ----- B ---- A' <--(origin/v1.0)              |
     \              \                               |
   .. M -- V ------- M <--(origin/v2.0)(v2.0)       |
            \         \                             |
         ... M -- W -- M <--(origin/master)(master) |
                        \                          /
                         C ---------------------- D <--(feature2)

这仍然将所有v2.0master更改合并到v1.0.

所以,你可以做什么?

v1.0您可以围绕不会收到冲突更改的乐观假设来构建您的工作流程。在那种情况下,你会

1) 创建分支v1.0 2) 进行更改 3) 快进v1.0feature( git merge --ff-only feature) 4) 尝试v1.0 不强制推送

现在,如果您尝试将更改合并到错误的分支中,则合并可能会失败(由于--ff-only. 只有当分支实际上已经分歧时,这才会有所帮助;但至少不会比现状更糟。

如果您确实转到了正确的分支,并且步骤 4 成功,那么您就完成了。如果第 4 步失败并给出有关非快进更改的错误,那么(因为这是一个例外,而不是您的预期步骤)它暗示您应该检查并确定它失败的原因。如果一切看起来都不错,那么接下来你可以

git pull --rebase

这是获取远程更改并将本地更改重新基于它们之上的简写。根据文档,这被认为是“潜在危险”的操作,但您正在做的事情也是如此;至少这会在它周围放置一些结构,这样只要你正确合并,它就会做你想做的事情。

然后,您应该始终习惯性地在推送之前快速查看本地结果 - 因为问题在推送之前总是更容易解决。所以看看日志,如果有什么奇怪的地方,请检查它。参考需求/故事/卡片/告诉您完成工作并验证您将其添加到哪个分支的任何内容。或许可以使用gitk.

最重要的是,git它非常灵活且非常强大,所以如果你明确告诉它做错事,它可能会做。这意味着你必须知道告诉它要做什么。好消息是,从错误中恢复通常并不。在错误被push纠正之前是最简单的,但或多或​​少总有办法。


推荐阅读