首页 > 解决方案 > 重新定位到远程并且缺少更改;发生了什么?

问题描述

我今天遇到了一个问题,我仍然无法弄清楚发生了什么。

我想从远程获取更改,然后重新设置它。这都在同一个分支中,让我们dev为了争论而说:

a --- b --- c --- e <-- local/dev

a --- b --- c --- d <-- remote/dev

我认为这样做的方法是:

git fetch
git checkout dev
git rebase remote/dev

我相当确定我过去曾这样做过。我期望的结果是:

a --- b --- c --- d --- e

提交消息似乎支持这确实是历史的状态,但是,其中包含的更改e不再存在。我无法解释这一点,也无法通过搜索互联网进一步理解它。

可能是我没有按照上面概述的步骤进行操作,也可能是必须有其他因素来解释发生了什么。我想我的问题是,发生的事情是否超出正常范围,还是必须有其他情况来解释?

也许 agit pull --rebase是这里更好的选择。

标签: git

解决方案


一定还有其他一些情况。(我不知道它们可能是什么。)

也许 agit pull --rebase是这里更好的选择。

那也是一样的。您的原始命令序列git checkout dev加上 command-pair git fetch; git rebase。什么git pull是运行,git fetch然后是第二个 Git 命令,通常git merge是第二个git pull --rebase运行git rebase。因此:

git checkout dev; git pull --rebase

是相同的:

git checkout dev; git fetch; git rebase

它只是短了四个字符(包括分号和空格)。

从坏的变基中恢复

请注意,您的原始提交仍然在您的存储库中可用。要找到它们,请使用以下两种机制之一:

  • ORIG_HEAD:这是在进行任何更改之前设置多个不同命令的标记。如果您所做的最后一件事是git rebase,ORIG_HEAD将是在进行更改之前保存的任何变基。(设置它的其他命令是git am,git reset和 - 有时 - <code>git merge,当它执行快进操作而不是执行合并时。)

  • reflogHEAD。_ 这存储了 的多个先前值HEAD。每一个都有编号和时间戳。旧条目最终会过期:默认情况下,Git 会确保在至少 30 或 90 天之前不会发生这种情况。在没有另一条非常有用的背景信息的情况下解释这里发生的事情有点棘手。

    (有关背景,请参阅Think Like (a) Git。真正发生的是可访问的 reflog 条目(可从引用的当前值访问,即 - 默认情况下具有 90 天的到期时间,而无法访问-from-the- ref 条目默认有 30 天到期。两者都是可调的,并且特殊refs/stash引用有不同的默认值:默认情况下,stash reflog 条目永不过期。)

除了只有一个ORIG_HEAD,并且 reflog 条目根据时间过期而不是被存储到的下一个值覆盖之外ORIG_HEAD,这两件事的工作方式几乎相同。

要查看来自 的提交ORIG_HEAD,请使用git log ORIG_HEAD(或与其他选项相同)。要查看 reflog 中的提交,请使用git reflog showor git log -g(git reflog show实际调用git log -g,以便您可以将其他git log选项传递给git reflog)。

例子

让我们看一下由于某种原因出错的变基——最常见的是需要修复太多合并冲突的变基。我们将从启动它的命令序列开始,但要明确地拼写成这样:

git checkout dev && git fetch && git rebase

git checkout dev命令将我们带到我们想要变基的分支。该git fetch步骤填写origin/dev,并且git rebase命令启动 rebase,这将复制我们的提交,dev而不是我们的origin/dev. &&s 确保每个命令在下一个命令开始之前成功完成 - 即使前一个命令失败,分号也会运行下一个命令。

副本将在提交到哪些origin/dev点之后进行。也就是说,在 之后git fetch,我们的存储库中可能会有这个提交图:

...--o--o--A--B--C   <-- dev (HEAD)
         \
          E--F   <-- origin/dev

我们想要结束的是:

...--o--o--A--B--C   [abandoned]
         \
          E--F   <-- origin/dev
              \
               A'-B'-C'  <-- dev (HEAD)

A'我们的副本在哪里AB'是我们的副本B,并且C'是我们的副本C

如果一切正常——或者至少,如果Git 认为一切正常——rebase 会以这样的方式结束:

...--o--o--A--B--C   <-- ORIG_HEAD, dev@{1}
         \
          E--F   <-- origin/dev
              \
               A'-B'-C'  <-- dev

设置在ORIG_HEAD变基完成后完成。这是rebase 完成后dev@{1}的 reflog 条目。dev(请注意,当我们执行其他命令时,条目 #1 会被下推到条目 #2、#3 等等,因此如果不是立即完成,您必须使用git reflog show或等效检查现在的数字是多少。 )

如果您已经完成了 rebase,并运行git log或查看结果或运行您的测试或其他任何东西并且感到恐惧并想要恢复原状,您现在可以运行:

git reset --hard ORIG_HEAD

或者:

git reset --hard dev@{1}

这两个都将:

  • 找到指定的提交,即提交的实际哈希值C
  • 使名称dev指向此提交(在此过程中将 reflog 条目向下推);和
  • 也重新设置索引和工作树(因为--hard),以便索引和工作树现在匹配 commit C

请注意,这git resetORIG_HEAD说明HEAD刚才所做的事情。也就是说,我们现在将拥有:

...--o--o--A--B--C   <-- dev (HEAD), dev@{2}
         \
          E--F   <-- origin/dev
              \
               A'-B'-C'  <-- ORIG_HEAD, dev@{1}

一个plain git log,从开始HEAD并向后工作,现在将向我们展示 commit C,然后B,然后A,然后是最右边的o,依此类推。

另一方面,假设我们开始rebase,并且已经到了这一点:

...--o--o--A--B--C   <-- dev
         \
          E--F   <-- origin/dev
              \
               A'-B'  <-- HEAD

我们正处于 rebase 的中间,试图挑选提交Cmake C',并遇到了一堆冲突。我们查看冲突并决定:毕竟不是这样做的时候。 我们想回到开始之前的状态。

git status命令将告诉我们,我们处于“分离 HEAD”模式,处于 rebase 的中间。我们跑:

git rebase --abort

这将停止我们的变基并为我们重新签出dev,给我们这个:

...--o--o--A--B--C   <-- dev (HEAD)
         \
          E--F   <-- origin/dev
              \
               A'  <-- HEAD@{2}
                \
                 B'  <-- HEAD@{1}

这一次,我在HEADreflog 条目中绘制了记住提交A'B'. 这些总是存在的——我们大多数时候只是将它们排除在图表之外,因为 reflog 条目通常是不可见的。这也是如此ORIG_HEAD:当我们不关心它时,我们会忽略它,因为git log除非我们明确要求它,否则不会看它。

另一个例子

假设你认为你已经完成了你的 rebase,但是要么退出它(git rebase --quit,一个相对较新的选项),要么实际上仍然在它的中间,有冲突。在这种情况下,您应该首先运行git status以确保事情是您认为的方式:

git status

如果这告诉您您正处于 rebase 的中间,您可以选择完成 rebase 或中止它,如上面的示例所示。

如果你真的完成了,你可以使用git reflog找到任何成功的部分cherry-picks,并创建一个新的临时分支指向它。例如,假设我们像以前一样成功地创建了A'B',看到了 的冲突C',并意外地终止了 rebase git rebase --abort。我们投入了大量工作来解决与他们的冲突,A'B'希望他们回来。现在我们运行:

git reflog

找到HEAD@{1},HEAD@{2}等等,以验证我们确实有:

...--o--o--A--B--C   <-- dev (HEAD)
         \
          E--F   <-- origin/dev
              \
               A'  <-- HEAD@{2}
                \
                 B'  <-- HEAD@{1}

既然B'是有价值的,让我们给它一个新的分支名称,例如new-dev

git checkout -b new-dev HEAD@{1}

现在我们有了这个图表,我们将在没有HEAD@{...}部分的情况下绘制它:

...--o--o--A--B--C   <-- dev
         \
          E--F   <-- origin/dev
              \
               A'-B'  <-- new-dev (HEAD)

我们可以恢复正常工作。最终,我们可以dev指出提交B'或新的C'或我们选择的任何内容;但就目前而言,我们可以一边工作new-dev一边继续工作dev

要记住的要点

  • 提交大多是永久性的且完全不可更改。它们的真名是它们又大又丑的hash ID,但人类不可能记住和处理它们,所以我们给它们起了名字。只要可以到达,它们就会一直存在(请参阅Think Like (a) Git)。

  • 分支名称是保存哈希 ID 的人类可读标识符。 我们选择名称;Git选择它们的值(底层提交的哈希 ID)。每当我们进行新提交时,当前分支名称的值都会自动更新。每个名称都指向 Git 应该在我们分支时显示的最后一次提交,以及 Git 应该在我们分支时检出提交。git loggit checkout

  • 使用git checkout分支名称会将名称附加HEAD到其中一个分支名称,以便新提交将更新该分支名称。使用git checkout提交哈希 ID,或使用不是分支名称的名称,会分离name HEAD,使其直接指向某个提交。

  • 使用git reset,我们可以移动当前分支以使其指向任何提交,或者如果我们处于分离 HEAD 模式,则移动分离HEAD(即名称HEAD本身)以指向任何提交。这样做将中止任何正在进行的合并、挑选或恢复。它不会(至少在现代 Git 中)终止正在进行的变基。Git 将保持在“分离的 HEAD”模式,而你的 rebase 实际上仍在继续。在这一点上使用git rebase --continueorgit rebase --skip可以放弃很多提交。

  • ORIG_HEAD就像 reflogs 的一个廉价(在所有意义上)变体:它会记住一个先前的提交,从最后一个让你感动HEAD很多的操作。

  • 真正的 reflogs——HEAD每个分支名称都有一个加一——保存许多以前的值。使用git reflog showorgit log -g与分支名称或HEAD您喜欢的名称一起显示这些分支或 的引用日志HEAD

  • git rebase作品:

    1. 列出要复制的提交。
    2. 分离 HEAD,使其指向副本所在的提交。
    3. Cherry-picking 1每个要复制的提交,一次一个。每个樱桃挑选都可能有冲突;如果是这样,Git 会停止并让您解决冲突。
    4. 在最后一次挑选完成后,移动分支名称以指向最后一次复制的提交。这将重新附加HEAD并设置ORIG_HEAD为分支名称的先前值,该值现在也在分支的 reflog 中。
  • git pull没有做任何特别的事情。这是一个方便的捷径。我建议避免使用它,但如果你真的确定要运行git mergegit rebase在 a 之后运行git fetch,那就是它会为你做的:运行git fetch,然后运行第二个 Git 命令。


1git rebase --interactive从字面上运行git cherry-pick;在现代 Git 中,两者都内置在 Git 内部调用的sequencer 中。其他一些变基模式也确实使用樱桃挑选。非交互式变基的默认值实际上是不同的路径,使用git format-patchgit am复制提交。从某种意义上说,这条路径略有缺陷,因为它不能处理重命名以及基于樱桃挑选的方法。


推荐阅读