【问题标题】:New to GitHub: Your branch and 'origin/master' have diverged, and have 1 and 2 different commits each, respectively. What should I do?GitHub 新手:您的分支和“origin/master”已经分歧,分别有 1 和 2 个不同的提交。我该怎么办?
【发布时间】:2020-11-07 07:05:56
【问题描述】:

我对 GitHub 很陌生(我坚持添加、提交和推送,并且没有玩过新的分支),今天正试图推送一些更改。但是,我提交了一些文件并意识到我搞砸了,并试图通过运行来取消提交:

git reset --mixed HEAD~;

我尝试了多次推送和重置。我不确定我做了什么,但在检查 git 状态时我最终来到了这里:

Your branch and 'origin/master' have diverged,
and have 1 and 2 different commits each, respectively.

当我尝试推送时,它指出:

hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

所以我认为我现在落后了很多,因为有一些文件已在最后两次提交中被跟踪,或者 git status 现在说是未跟踪的。此外,我不想丢失我在本地计算机上取得的任何进展。我怎样才能快进并推动我想做的更改,最好不要丢失任何过去的提交或当前的进度?

【问题讨论】:

    标签: git github merge branch


    【解决方案1】:

    所以我认为我现在落后了很多,因为有一些文件已在最后两次提交中被跟踪,或者 git status 现在说是未跟踪的。此外,我不想丢失我在本地计算机上取得的任何进展。我怎样才能快进并推动我想做的更改,最好不要丢失任何过去的提交或当前的进度?

    让我们将它们分成不同的组件,以便您能够正确理解发生了什么。我们在这里关心的组件是:

    • 存储库:您的计算机上有一个,GitHub 上有一个。
    • 在每个存储库中:
      • 提交
      • 分支名称

    提交实际上是共享的,而分支名称却没有——它们以更奇特的方式处理。但是提交只会在特定的连接点共享,当你让你的 Git 在 GitHub 上调用 Git 时;然后你的 Git 和他们的 Git 就这些分支名称和提交进行一些对话。让我们把它留到以后,首先关注你的 Git 存储库中的内容:提交和名称。

    名字

    通常我先从提交开始,但这次,让我们从名称开始。除了 branch 名称之外,还有更多种类的名称,我们稍后再讨论,但现在,让我们关心一下分支名称是什么以及做什么。

    分支名称只是一个名称 - 一系列字母,最好是和/或数字,有一些规则阻止您使用 br..an..ch 之类的东西作为分支名称,但允许使用 bra.nch。这个分支名称的主要作用是保存 one 提交哈希 ID。这是 latest 提交的哈希 ID。所以没有 commits,名字实际上对你没有任何好处。

    提交

    提交是 Git 真正的核心功能。几乎所有内容都与提交有关。提交保存文件的版本,永远 - 或至少只要这些提交继续存在 - 但重要的是要了解 如何 提交做到这一点,以及 Git 如何发现 一次提交。

    让我们从一个简单的想法开始这部分:每个提交都有编号。然而,这些数字并不是简单的数数。它们不是提交 1、2 和 3。相反,每个数字都是一个丑陋的 散列 ID。它看起来完全随机(尽管实际上它完全是非随机的)。从最新的提交(例如,4a0fcf9f760c9774be77f51e1e88a7499b53d2e2)与之前的提交之间没有明显的联系。

    找到一个提交,你需要知道它的编号。但是这些数字看起来是随机的,而且对于人类来说太大太丑了以至于无法记住。这就是我们有分支名称的原因:它们记住 last 提交的编号。但是等等:只知道 last 提交有什么好处?好吧,让我们看看每个提交的内容。

    每个提交都有两部分:它有它的数据,它是您文件的所有的完整快照。我们稍后会回到这个话题。然后它有一些元数据,或者关于提交本身的信息。在此元数据中,您将找到提交人的姓名、何时他们提交,以及为什么他们提交:他们的日志消息。但 Git 还会存储并查找 Git 本身想要的另一条信息,那就是这个提交的 parent 提交的编号(哈希 ID)。

    任何提交的父提交是在该提交之前的提交。因此,对于普通的单亲提交(往往是其中的大多数),这意味着 Git 可以从 last 提交开始并简单地向后工作。这正是 Git 所做的,我们可以这样绘制:

    A <-B <-C   <--master
    

    这里我们有一个简单的存储库,其中只有三个提交,都在一个名为 master 的分支上。 name master 持有最后一次提交的哈希 ID——我们称之为 C 尽管它有一些大而丑陋的哈希 ID——而提交 C 持有的哈希 ID较早提交B。所以Git可以使用name找到C,然后使用C找到B

    与此同时,B 持有早先提交 A 的哈希 ID,因此在找到 B 后,Git 可以找到 AA 是任何人的第一个提交,所以它没有父级。这让 Git 停止向后工作。

    提交和分支

    这里还有一个更有趣的皱纹,一旦我们拥有多个分支,就会出现这种情况。假设我们的存储库中有多达 8 个提交:

    ...--G--H   <-- master
    

    我已经不再费心在提交之间绘制反向箭头了。没关系,因为所有提交都必须向后指向,而且提交还有另一件关键的事情:一旦你做出提交,其中的任何内容都不会改变。1 所以向后 -指向箭头被冻结,无法添加前向箭头。但是,分支名称并非如此:记住,master 曾经包含提交 C 的真实哈希 ID;现在它包含提交 H 的实际哈希 ID。

    如果我们现在创建一个 分支名称,新名称将也指向提交 H2 让我们画出:

    ...--G--H   <-- develop, master
    

    我们实际使用的是哪个名称? Git 为我们提供了一个答案:我们应该将特殊名称 HEAD 附加到我们想要使用的分支上,像这样全部大写,3。所以:

    ...--G--H   <-- develop, master (HEAD)
    

    表示我们使用名称master,它选择提交H,而:

    ...--G--H   <-- develop (HEAD), master
    

    表示我们使用名称develop,它选择提交H


    1它被冻结的原因是提交的编号——它的哈希 ID——是通过计算数据和元数据中所有位的安全哈希来构建的。这意味着实际上不可能更改提交。如果你拿出一个,把它变成普通数据,稍微改变一下,然后写回来,你会得到一个新的和不同的提交。原始提交保留在存储库中;您只是添加了一个新的、不同的提交。

    2实际上,我们可以选择 any 现有的提交来命名。但是,我们必须选择一些现有的提交:你不能有一个不指向一些现有提交的分支名称。

    3在某些系统上,您可以输入小写的head 并让它工作。这是一个坏习惯,因为:

    • 它不适用于所有系统,并且
    • 当您开始使用 git worktree 时它会中断。

    如果您不喜欢输入单词HEAD,请考虑使用单字符同义词@


    进行新的提交,第 1 部分

    HEAD 很重要的一个地方是当我们进行 new 提交时。假设我们有:

    ...--G--H   <-- develop (HEAD), master
    

    无论哪种方式,我们都使用提交H。但是,如果我们更改了一些文件并git add 它们和git commit,这会告诉 Git 进行 new 提交。让我们调用新的提交 I 并将其绘制,如下所示:

    ...--G--H
             \
              I
    

    请注意,新提交 I 是现有提交 H。那是因为我们从提交 H 开始。

    分支名称会发生​​什么变化?答案是:Git 自动更新 HEAD 所附加的名称。由于HEAD 曾经并且仍然附加到develop,因此现在指向新提交I 的名称:

    ...--G--H   <-- master
             \
              I   <-- develop (HEAD)
    

    如果我们现在 git checkout master 回到 master,Git 会将我们返回到现有提交 H,将特殊名称 HEAD 附加到 master,并给我们:

    ...--G--H   <-- master (HEAD)
             \
              I   <-- develop
    

    提交是只读的,那么文件如何工作?

    我们之前提到过,提交是所有文件的实时快照。为了使这项工作顺利进行,Git 以特殊的、只读的、仅 Git 的、冻结的和去重复的格式存储每个文件。只有 Git 本身可以使用这些文件。因此,当我们选择要使用的提交时,Git 将文件从提交中复制到工作区。那个工作区,里面有普通的日常文件,就是你的工作树工作树

    重复数据删除意味着即使每次提交都有每个文件的完整快照,大多数快照只是重复使用现有文件。也就是说,当我们提交I 时,我们可能更改了一两个文件,而其他所有文件都保持不变。所以提交 I 和提交 H 实际上 share 他们的大部分文件。在某种程度上,他们可能也与较早的提交共享大部分那些。 (事实上​​,如果您将文件 back 更改为之前提交时的方式,新文件会自动与旧提交共享。)

    这就是每次提交中的数据:所有文件的完整、冻结快照,或者更确切地说,是你告诉 Git 放入该快照的所有文件的完整快照。那是哪些文件呢?

    进行新的提交,第 2 部分

    每个提交都包含这些冻结格式的文件,这些文件需要扩展到您的工作树中。那么,您可能会假设 git commit 获取您工作树中的任何内容并提交它。但实际上 Git 的工作方式并非如此。

    除了原始的、提交中的、冻结的文件和工作树中的日常文件之外,Git 还保留了每个文件的中间副本4额外的副本位于一个非常重要或最初命名非常糟糕的区域中,以至于它具有三个名称。 Git 称它为 index,或 暂存区,或者有时——现在很少见——缓存。我将在这里使用术语 index,但请记住 staging 指的是这些额外的副本。

    每个文件的索引副本都处于冻结格式,准备进入下一次提交。所以这意味着考虑 Git 索引的一个好方法是它包含 提议的下一次提交git add 命令告诉 Git:将工作树、文件的普通格式副本复制到或返回到索引中,替换之前的任何副本。 这也将其准备为冻结格式(删除重复),以便为下一个 git commit 做好准备。

    当您运行 git commit 时,Git 会收集元数据所需的任何额外信息(例如您的姓名和日志消息),然后构建新的提交。然后它写出索引中的任何内容当时作为快照,添加元数据(包括当前提交作为新提交的父级)并进行新提交,然后更新任何分支名称@987654384 @ 已附加到。

    如果您喜欢使用git commit -a,请注意这只会使git commit 更新已在索引中的文件。这几乎等同于运行git add -u(更新已知文件),然后运行git commit5

    您无法直接看到索引,6git status 隐含地告诉您索引中的内容。 git status 的工作方式也很简单:

    • 你有一个当前提交。那就是您的分支名称(通过查看HEAD 找到)所说的是 last 提交。该提交中有一堆文件,采用 Git 特殊的、内部的、冻结的格式。 Git 也将此称为HEAD 提交。

    • Git 有它的索引。其中有一堆文件,采用 Git 的特殊冻结格式——但与提交中的文件不同,它们可以替换为新副本。

    • 而且,你有你的工作树,你可以在其中做任何你想做的事——包括创建全新的文件。

    git status 命令进行两个单独的比较:

    • 首先,它将HEAD 提交中的所有文件与索引中的所有文件进行比较。对于每个相同的文件,它什么也没说。对于每个不同的文件——包括新的或消失的——它表示该文件已暂存

    • 然后git status 将 Git 索引中的所有文件与工作树中的文件进行比较。对于每个相同的文件(从冻结形式展开后),它什么也没说。对于每个不同的文件,它表示该文件没有为提交暂存

    这意味着您可以查看索引中可以更新的内容,而无需查看索引中与

    中的副本相同的每个文件>

    4从技术上讲,索引中的内容不是文件的副本,而是对冻结格式 Git 内部的 引用 blob 对象。但是你通常不需要知道这一点——只有当你开始使用 git ls-files --stagegit update-index 来处理 Git 的低级索引时才重要。

    5这里的主要区别在于,如果新提交失败git commit -a 方法会回滚索引。使用git add -u 是一个单独的步骤,因此如果添加有效且提交失败,索引仍会更新。还有许多更微妙的区别,但我们将在这里忽略所有棘手的极端情况。重要的一点是,Git 从索引进行提交,通常只有一个索引—— 索引——而其他一切都来自该索引。

    6实际上,您可以查看索引中的内容:运行git ls-files --stage。请注意,这会将大量输出转储到大型存储库中!这个命令不是你通常使用的命令:它是供 Git 程序在内部使用的,而不是供用户使用的。


    跟踪和未跟踪的文件

    现在您知道 Git 从其索引中进行提交,您终于可以正确理解已跟踪和未跟踪的文件了。 跟踪文件的定义非常简单,但仍然很复杂:跟踪文件是当前在 Git 索引中的文件。

    您可以随时将文件添加到索引中:git add <em>newfile</em>。现在跟踪该文件。您也可以随时从索引中删除文件:git rm --cached <em>oldfile</em>--cached 阻止 git rm 删除您的工作树副本,以便您仍然可以查看该文件,但它不再在 Git 的索引中:该文件现在未跟踪。

    但请记住:git checkout <em>branch</em> 告诉 Git 从某个现有提交中填充其索引和您的工作树!所以 Git 会自己更新它的索引。如果 Git 的索引和您的工作树中现在有文件,并且您 git checkout 提交 没有 有这些文件,Git 将从中删除这些文件它的索引和您的工作树,以便您看到该提交中保存的内容。

    未跟踪的文件 是您的工作树中的任何文件,但不在 Git 的索引中。当你有这样一个文件——一个不在 Git 的索引中——并且 git checkout 一些其他提交也没有该文件时,该文件仍然不在 Git 的索引中,因此继续未被跟踪。

    (当您有一个未跟踪的文件时,会出现一个偷偷摸摸的情况,然后要求切换到 确实 具有该文件的某个提交。我们不会在这里担心,但您可能会看到这怎么可能是个问题。)

    摆脱提交

    提交实际上很难摆脱(除了删除整个.git 目录,这会丢失所有内容,而且很少是一个好主意)。那是因为 Git 是为了添加新的提交 而构建的,而不是为了删除它们。但实际上你可以摆脱提交。

    假设一些分支名称和一些提交系列:

    ...--G--H   <-- master (HEAD)
    

    现在进一步假设我们可以让 Git 相信 name master 应该保留,而不是提交 H 的哈希 ID,而是提交 G 的哈希 ID,如下所示:

           H
          /
    ...--G   <-- master (HEAD)
    

    请注意,提交 H 实际上仍然存在,在存储库中。但 Git 我们展示了从 ID 存储在 name 中的提交开始,例如 master。现在的名称显示 commit G 是最新的提交。提交G 指向一些较早的提交(可能是F)。因此,如果我们向 Git 询问此存储库中的提交,我们将不会再看到提交 H

    (当我们绘制这些时,我们可以向上或向下推动“丢弃的”提交以将它们移开。我受 StackOverflow 文本约定的限制,但如果你在纸上或白板上绘制这些,请随意以任何方式绘制它们,包括从分支名称到提交的长俯冲箭头。)

    请注意,这仅适用于“尾”提交。也就是说,假设我们有:

    ...--G--H   <-- master (HEAD)
             \
              I   <-- develop
    

    如果我们强制名称master 指向提交G,那么提交I 仍然指向提交H,所以我们得到的是:

    ...--G   <-- master (HEAD)
          \
           H--I   <-- develop
    

    也就是说,现在看起来我们在分支develop 上提交了H。尽管如此,分支master 现在在提交G 处结束,所以我们肯定做了一些事情

    这就是git reset 所做的。当你跑的时候:

    git reset --mixed HEAD~

    你告诉你的 Git:找到比 HEAD 后退一步的提交——从当前分支的最后一次提交后退一步。然后,强制当前分支名称识别该提交。 如果你有:

    ...--G--H--I   <-- master
    

    你这样做一次,你会得到:

    ...--G--H   <-- master
             \
              I
    

    如果你再次这样做,名称master 指向提交G,而H-I 将悬空。默认情况下,在您的存储库中,它们会保留一段时间——用户存储库至少有 30 天的时间来取回这些提交。 (这里的机制是 Git 称之为 reflogs 的东西,但我们不会详细介绍。)

    git reset--mixed 参数告诉 Git 在移动事物时保持工作树不变。因此,工作树中的文件副本将被单独保留。使用--hardgit reset 也可以调整这些。使用--softgit reset 单独保留 Git 的索引,但使用 --mixed,Git 清空旧索引并从您选择的提交中填充它。

    这 - 替换索引,但不理会工作树 - 很容易导致未跟踪文件的情况。特别是,假设提交 I 添加了一个不在提交 H 中的 new 文件。然后上面的 reset 从 Git 的索引中删除新文件,将新文件留在工作树中。该文件现在在您的工作树中,但不在 Git 的索引中,这就是未跟踪文件的定义。

    请记住,所有已提交文件在这些提交中都是安全的,只要您仍然可以找到这些提交。通过使很难找到像 commit I 这样的提交,您已经进行了设置,因此您可能无法轻松获取 那些 版本的文件。但是git log 向您展示的任何提交,那些 提交都很容易找到。 (我们已经跳过了使用 Git 的 detached HEAD 模式来查看历史提交的想法,以便不必覆盖该模式,但这是查看历史版本的一种方式。)

    添加更多 Git 存储库

    既然您知道提交和分支名称在您的 存储库中的工作方式(包括添加新提交和重置一些提交),是时候将 GitHub Git 添加到组合中了。 p>

    要让您的 Git 调用其他 Git,您需要有一个 URL,例如 ssh://git@github.com/...https://github.com/...。您的 Git 会为您保存这个 URL,使用一个好记的简短名称。 Git 将其称为远程。许多 Git 存储库只有一个远程,称为 origin,我假设您的也是这种情况。

    要让您的 Git 与另一个 Git 连接,您将运行以下三个命令之一:git fetchgit pushgit pullgit pull 命令只是一个方便的包装器,它首先运行 git fetch,然后运行第二个 Git 命令,而且最好——嗯,认为最好——分别学习 git fetch。所以这给了我们两个让 Git 互相交流的命令。

    两个命令本身比较简单:

    • git fetch 让你的 Git 调用他们的 Git,然后问他们有什么,而你没有。他们列出了他们的分支名称(和其他名称)和他们的提交哈希 ID。您的 Git 可以立即判断您是否有这些提交,因为 每个 Git 存储库中的哈希 ID 都是相同的(再次参见脚注 1)。如果您没有提交,您的 Git 会要求他们的 Git 将它们发送过来。他们这样做了,现在你也有了提交。

      现在您已经拥有了他们拥有的所有提交(以及您自己的任何未共享的提交),您的 Git 将创建或更新您的 origin/* 名称,以记住他们所拥有的他们的分支名称。您的每个origin/* 名称都是一个远程跟踪名称7这些只是您的 Git 对他们在哪个分支名称中拥有的哈希 ID 的记忆,当时您跑了git fetch

      如果他们不更改他们的分支名称(永远或经常),您的git fetch 将每次都正确设置您的远程跟踪名称。如果他们确实经常更改它们,您需要经常运行 git fetch 以获取任何新名称和不同的提交哈希 ID。

      您可以像这样运行git fetch,根本不需要任何参数。

    • git push 让你的 Git 调用他们的 Git,然后如果需要给他们新的提交。这比git fetch 稍微复杂一点,因为为了让他们记住任何新的提交,他们必须更新他们的分支名称!它们没有您的远程跟踪名称。

      git fetch 一样,您的 Git 会列出提交哈希 ID。他们检查是否有这些 ID。如果没有,他们会让你的 Git 发送这些提交(以及他们的文件,如果需要的话——这里有很多花哨的东西来避免发送他们已经有副本的文件)。和以前一样,每个 Git 都使用 same 哈希 ID 进行相同的提交这一事实使得这很容易。

      然后,一旦他们的 Git 需要任何提交,您的 Git 就会发送一个或多个请求:如果可以,请将您的分支名称 ______(填写名称)设置为 ______(填写哈希 ID )。是否服从这个礼貌的要求取决于他们。或者,您的 Git 可以发送命令:将您的名字 _____ 设置为 _____! 是否服从仍然取决于他们。

      git push 命令需要8,您输入的名称为 origin,因为很久以前,有人说语法将是 git push <em>remote branch</em>,所以您必须输入 @ 987654472@ 在你输入分支名称之前。然后,git push 需要分支的名称。这告诉您的 Git 要发送哪个提交(您的 Git 像往常一样从您的分支名称中找到 last 提交)并填写两个空白。也就是说,我们必须将分支名称 哈希 ID 都放在两个空格中,用于礼貌请求或强制命令。您的 Git 从您在此处输入的名称中获取这两个名称。9

      他们告诉我们的 Git 他们是否服从了命令。如果是这样,我们的 Git 会更新与他们的分支名称相对应的一个远程跟踪名称。也就是说,如果我们让他们更新他们的master,我们的 Git 会更新我们存储在origin/master 中的内存。由于我们没有找到他们的任何 其他 分支名称,因此我们的其他 origin/* 名称都不会更新。


    7Git 称这些东西为 remote-tracking branch names,但我发现这里的 branch 这个词让事情变得更混乱,而不是更少;所以现在我把它放在一边,只称它们为远程跟踪名称

    8可以进行设置,以便git push 默认推送当前分支,然后您可以忽略它—— Git 会找出正确的远程分支和当前分支——但我喜欢显示明确的版本。

    9还有其他可用选项,让您变得更漂亮。例如,如果您愿意,您可以从您的姓名 grandpa-simpson 推送到他们的姓名 onion-on-my-belt。可以在每一侧使用完全不同的名称。但是不要在没有强烈需求的情况下这样做:它很快就会变得非常混乱。


    快进和非快进

    现在让我们想象一下,我们是接收git push 的Git。其他一些 Git 打电话给我们,问我们是否有提交 a123456。我们没有,所以他们给了我们。 a123456 有父 9876543,我们确实有,所以这是我们唯一需要的提交。现在他们说:如果可以,请将您的 master 设置为 a123456

    让我们画出我们所拥有的:

    ...--G--H   <-- master
    

    假设提交H 的哈希ID 9876543。那么新的提交a123456 显然是一个新的提交I,它只是添加到我们现有的master,我们可以这样放入:

    ...--G--H--I   <-- master
    

    但是如果父级9876543 不是 提交H 怎么办?如果它是提交G 怎么办?也就是说,我们有:

    ...--G--H   <-- master
    

    他们给了我们:

    ...--G--H
          \
           I
    

    他们现在要求我们将master 设置为记住提交I?如果我们这样做,我们将失去我们的提交H。我们最终会得到:

           H
          /
    ...--G--I   <-- master
    

    我们将无法再找到提交H。所以我们会对礼貌的请求说no,因为这个操作不仅添加提交到我们的master,它还删除提交。

    如果他们向我们发送一个强有力的命令——将你的 master 设置为 a123456——我们可能会服从它,并放弃提交 H。如果他们没有保留提交H 的副本,它可能会很快消失。服务器端存储库通常没有 reflog,并且几乎可以立即删除被放弃/悬空的提交。

    你自己的情况

    我们可以描绘出你的情况,其中你的master 是你的origin/masterahead 1, behind 2——你的Git 对他们Git 的master 的记忆。它可能看起来像这样:

              I--J   <-- origin/master
             /
    ...--G--H
             \
              K   <-- master (HEAD)
    

    如果愿意,您可以使用git push --force origin master 将您的提交K 发送给他们,并告诉他们放弃他们的I-J 提交。但是,如果您想保留这些提交,请不要​​这样做。

    如果你愿意,你可以放弃自己的提交 K:

    git reset [options] origin/master
    

    会给你:

              I--J   <-- master (HEAD), origin/master
             /
    ...--G--H
             \
              K   [abandoned]
    

    您的提交 K 会保留一段时间,尽管找到它会有点困难。如果你根本不想要它,那可能没关系。

    您可以使用git merge 组合来自提交H 与提交K 的更改以及来自提交H 与提交J 相比的更改,以进行新的提交。您可以使用git rebase 将现有提交K 复制 到添加到提交J 的新的不同提交。您可以做很多事情。每个都有一组不同的结果提交。记住提交是什么,为你做了什么,分支名称如何找到提交,并决定你想要哪些提交,你想假装哪些提交从未发生过——用git reset 和/或git push --force 删除——然后设置以您想要的方式在您自己的本地存储库中。然后使用git push,带或不带--force,将新提交发送到GitHub Git,并让他们设置他们的分支名称以指向新提交,以匹配您自己的设置你的 Git。

    【讨论】:

    • 谢谢!我最终使用了 git rebase。这都是很酷的东西,我迫不及待地想了解更多关于它的信息,因为我一直在使用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-28
    • 2015-12-14
    • 2011-01-27
    • 2012-04-27
    • 1970-01-01
    • 2017-05-26
    相关资源
    最近更新 更多