Git 变基一个不包含新提交的分支,不需要 —force 需要推送?

Git rebasing a branch that contains no new commits, no —force needed for pushing?

抱歉,如果这个问题已经被 awnser 解决了,但我找不到 awnser。

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

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

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

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

  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 push v1.0 分支。 我对 rebase 的理解是,当你进行 rebase 时,你会重写分支的历史,因此你需要使用 git push —force,否则远程将不会接受你的更改。 在这种情况下是否没有发生历史重写,因为 master 已经包含了 v1.0 分支的所有提交以及 v1.0 分支不包含的一堆额外提交?

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

基本没有。当你变基到一个分支时,你使用那个(整个)分支作为被变基的提交的根(起点)。

所以您没有更改您刚刚添加到其中的 v1.0 的现有历史记录。无需强制推动。

未来如何防止这种情况的最佳建议是避免变基。从 v1.0 分支以创建修复。您已经在使用 merge 将更改引入 master。

这有点长,因为您提出要真正了解正在发生的事情,所以我将提供更多信息,而不仅仅是直接回答您的问题。但是,如果您不采取任何其他措施:在推送之前验证您的本地状态。 (紧随其后:对力推持怀疑态度。)


人们习惯认为"rebase" == "need to force push",两者在某种程度上是相关的;但不仅仅是重新设置基准的行为会产生强制推动的需要。这是 从分支历史记录中删除提交的行为(比如 branchX),然后 只有 branchX 需要强制推送 .

考虑到这一点,让我们来看看您的工作流程 - 首先是它的预期工作,然后是它发生时出现的这个错误。作为起点,您的回购协议可能看起来像

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

这里...表示"some history we don't really care about",x表示"a commit, but not one I'm going to specifically reference by name in this discussion",M表示"a merge commit, but not one I'm going to specifically reference by name in this discussion"。其他字母表示我可能会通过名称引用的提交。如果我可以按名称引用合并,我会将其命名为 M1。然后 /\-- 显示提交之间的父子关系(较新的提交在右边),括号中的名称是带有箭头的引用(例如分支)显示裁判的当前提交。

除了本地分支之外,我还展示了远程跟踪参考 - 即您的仓库对远程分支位置的理解。

所以...

预期行为

1) 从 v1.0 分支出来

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

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

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 "moved",因此 A 已从其历史记录中删除,而 B 和新提交(A' - 补丁-A) 的明智副本已添加到其历史记录中。

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

无论如何,所有其他裁判保留他们已有的所有历史记录。

4) git push -f origin feature_branch

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

也就是说,改进我上面所说的 - 仅当推送您已从其历史记录中删除了提交的引用时才需要强制推送这是该分支的远程历史记录的一部分.

所以让我们假设在重新设置基准之前 已经 推送了 feature_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 的真正原因。)现在如果没有 -f 标志,您将无法推送 feature_branch,因为推送会删除 A来自远程feature_branch历史的理解。

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

也就是说,post-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。因为它是快进(即使是远程),所以不需要强制推送;也就是说,远程 ever 视为 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 提交添加不同的名称,并删除一些我们不会谈论的引用

... 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 历史记录中的所有内容都被视为 "need to copy".

                     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 的默认行为;无论如何它们都不会引入明显的变化,尽管冲突解决方案 and/or "evil merges" 可以打破这个假设)。但是 VW 不会被忽略。与往常一样,请注意 VW 仍然存在,每个分支的历史 除了 您重新建立基础的当前分支保持不变。

与上述工作流程一样,您现在可以推送 feature。和上面一样,如果你在变基之前曾经 pushed feature,你现在必须强制推动它...... 但是你典型的工作流程让你误以为无论如何,所以虽然它应该发出危险信号但它不会。

无论哪种方式,您都可以看到 v1.0 会愉快地快进到 feature(因为 feature 的历史无论如何都包含 v1.0 的所有历史), 这意味着 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 步失败并给出有关非快进更改的错误,那么(因为这是一个异常而不是您预期的步骤)它提示您应该检查并确保 why 它失败的。如果一切正常,那么接下来你可以

git pull --rebase

这是一个 shorthand,用于获取远程更改并在它们之上重新设置本地更改。根据文档,这被认为是 "potentially dangerous" 操作,但您正在做的也是如此;至少这在它周围放置了一些结构,所以只要你正确合并,它就会按照你的意思去做。

那么你应该总是,作为一个习惯,在推送之前快速查看本地结果 - 因为问题在推送之前总是更容易解决。因此,请查看日志,如果有任何异常,请进行检查。回头参考 requirement/story/card/whatever 告诉你做的工作并验证你将它添加到哪个分支。也许可以使用 gitk.

等工具可视化回购协议的整体状态

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