GitHub 的新手:你的分支和 'origin/master' 已经分叉,分别有 1 和 2 个不同的提交。我应该怎么办?

New to GitHub: Your branch and 'origin/master' have diverged, and have 1 and 2 different commits each, respectively. What should I do?

我是 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 状态现在表示未被跟踪。此外,我不想丢失我在本地计算机上取得的任何进展。我怎样才能快进并推动我想做的改变,理想情况下不会丢失任何过去的提交或当前的进展?

So I think I'm now behind quite a bit because there are some files that have been tracked for the last two commits or so that git status is now saying are untracked. Furthermore, I don't want to lose any of my progress that I've made locally on my computer. How can I fast forward and push the changes I'd like to make, ideally without losing any past commits or current progress?

让我们将它们分成不同的部分,这样您就可以正确地理解发生了什么。我们这里关心的组件是:

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

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

名字

通常我先从提交开始,但这次,我们只从名称开始。除了 b运行ch 名称之外,还有更多种类的名称,我们稍后会回过头来讨论,但现在,让我们只关心 b[=971= 是什么]ch name is and does.

A b运行ch 名字只是一个名字——一系列字母,最好是 and/or digits,有一些规则阻止你使用像 br..an..ch 作为 b运行ch 名称但允许 bra.nch。这个 b运行ch 名称的主要功能是保存 one 提交哈希 ID。这是 最新 提交的哈希 ID。因此,如果没有 提交 ,名称实际上对您没有任何好处。

提交

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

让我们从一个简单的想法开始这部分:每个提交都有编号。然而,这些数字并不是简单的计数。它们不是提交 1、2 和 3。相反,每个数字都是一个丑陋的大 哈希 ID。它 看起来 完全 运行dom(尽管实际上它完全是 non-random)。最新的提交(其编号为 4a0fcf9f760c9774be77f51e1e88a7499b53d2e2)与其之前的提交没有明显的联系。

找到 一个提交,您需要知道它的编号。但是这些数字看起来 运行dom,又大又丑,人类无法记住。这就是我们有 b运行ch 名字的原因:他们记得 last 提交的编号。但是等等:只知道 last 提交有什么好处?好吧,让我们看看每个提交里面都有什么。

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

任何提交的父项都是之前的提交。所以对于普通的 single-parent 提交——这往往是其中的大部分——这意味着 Git 可以从 last 提交开始,然后简单地向后工作。 Git就是这样做的,我们可以这样画:

A <-B <-C   <--master

这里我们有一个简单的存储库,里面只有三个提交,都在一个名为 master 的 b运行ch 上。 name master 保存最后一次提交的哈希 ID——我们称之为 C,尽管它有一些丑陋的大哈希 ID——并且那个提交 C 保存早期提交的哈希 ID B。所以Git可以用nameC,然后用CB.

同时B保存了较早提交A的哈希ID,所以找到B,Git可以找到AA 是任何人所做的第一个提交,所以它没有父项。这让 Git 停止向后工作。

提交和 b运行ches

这里还有一个有趣的问题,一旦我们有多个 b运行ch 就会出现这种情况。假设我们的存储库中最多有八次提交:

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

我已经不再费心在提交之间绘制向后箭头了。这没关系,因为所有提交都必须向后指向,并且提交还有另一件关键的事情:一旦你提交,其中的任何内容都不会改变。1 因此 backwards-pointing 箭头被冻结,无法添加 forwards-pointing 箭头。 b运行ch 名称 并非如此:重新ember,master 用于包含提交的真实哈希 ID C;现在它包含提交的实际哈希 ID H.

如果我们现在创建一个 new b运行ch 名称,新名称将 也指向提交 H2 让我们画一下:

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

我们实际使用的是哪个名称? Git 为我们提供了答案:我们应该在 b运行ch 上附加特殊名称 HEAD,全部大写,3我们想要使用。所以:

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

表示 我们正在使用名称 master,select 提交 H,而:

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

表示 我们正在使用名称 develop,select 提交 H.


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

2实际上,我们可以选择 任何 现有提交来命名。但是,我们必须 选择一些现有的提交:不允许使用不指向某些现有提交的 b运行ch 名称。

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

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

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


进行新的提交,第 1 部分

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

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

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

...--G--H
         \
          I

请注意,新提交 I 父级 是现有提交 H。那是因为我们从提交 H.

开始

b运行ch 名称发生了什么变化?答案是:Git自动更新HEAD附上。由于 HEAD 曾经并且仍然附加到 develop,因此该名称现在指向新提交 I:

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

如果我们现在 git checkout master 返回到 master,Git 将 return 我们到现有提交 H,附加特殊名称 HEADmaster,然后给我们:

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

提交是 read-only,那么文件是如何工作的?

我们之前提到,提交是 所有 文件的 frozen-in-time 快照。为了使这项工作顺利进行,Git 以特殊的 read-only、Git-only、冻结和 de-duplicated 格式存储每个文件。只有 Git 自己才能 使用 这些文件。因此,当我们选择要使用的提交时,Git 将文件从提交 复制到工作区。那个工作区,里面有普通的日常文件,是你的 work-tree 或者 working tree.

de-duplication 意味着即使每个提交都有 每个 文件的完整快照,这些快照中的大多数只是 re-using 现有文件。也就是说,当我们提交 I 时,我们可能更改了一两个文件,而其他所有文件保持不变。所以提交 I 和提交 H 实际上 共享 他们的大部分文件。在某种程度上,他们可能也与早期提交共享大部分 those。 (事实上​​ ,如果您将文件 改回 到它在某个早期提交中的方式,新文件会自动与旧提交共享。)

这是每个提交中的数据:所有 文件的完整冻结快照,或者更确切地说,是您告诉 Git 放入的所有文件的快照那张快照。那么这些是哪些文件?

进行新的提交,第 2 部分

每个提交都包含这些 frozen-format 文件,这些文件需要扩展到您的 work-tree 中。那么,您可能会假设 git commit 获取了 work-tree 中的任何内容并提交了它。但实际上 Git 并不是这样工作的。

除了原始文件、in-commit、冻结文件和在-work-tree、日常文件之外,Git 还保留了一个中间副本每个文件。4 这个额外的副本位于一个非常重要的区域,或者最初命名如此糟糕的区域,以至于它有三个名称。 Git 称其为 index,或 staging area,或者有时——现在很少——cache。我将在这里使用术语 index 但请记住 staging 指的是 t这些额外的副本。

每个文件的索引副本都是冻结的格式,准备进入下一次提交。所以这意味着考虑 Git 索引的一个好方法是它包含 提议的下一次提交 git add 命令告诉 Git:复制 work-tree 文件的普通格式副本到索引中,或返回到索引中,替换任何以前的副本。 这也以冻结格式准备它(de-duplicating 它也是)以便它为下一个 git commit.

做好准备

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

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

您不能直接看到索引,6 但是 git status 会隐式地告诉您索引中的内容。 git status 的工作方式也很简单:

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

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

  • 而且,您有自己的 work-tree,在这里您可以做任何您想做的事情——包括创建 all-new 文件。

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

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

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

这意味着您可以查看索引中可以更新的内容,而不必查看索引中相同的每个文件作为

中的副本

4从技术上讲,索引中的内容不是文件的副本,而是参考 到 frozen-format Git 内部 blob 对象 。但是您通常不需要知道这一点——只有当您开始使用 git ls-files --stagegit update-index 来处理 Git 的 low-level 索引时才重要。

5这里的主要区别在于,如果进行新提交失败git commit -a方法会回滚索引.使用 git add -u 是一个单独的步骤,因此如果添加成功但提交失败,索引仍会更新。还有很多 more-subtle 区别,但我们将忽略这里所有棘手的极端情况。重要的一点是 Git 从一个索引进行提交,通常只有一个索引—— 索引——其他所有内容都从那个开始。

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


跟踪和未跟踪的文件

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

您可以随时将文件添加到索引中:git 添加<em>newfile</em>。现在跟踪该文件。您也可以随时从索引中删除 文件:git rm --cached <em>oldfile</em>. --cached 阻止 git rm 删除您的 work-tree 副本,这样您仍然可以 查看 该文件,但它不再位于 Git 的索引:该文件现在未被跟踪。

但是记住:git checkout <em>b运行ch</em> 告诉 Git 到 填写它的索引和你的work-tree 来自一些现有的提交!所以 Git 将自行更新其索引。如果 Git 的索引和你的 work-tree 中现在有文件,而你 git checkout 提交 没有 有这些文件, Git 将 从其索引和您的 work-tree 中删除 那些文件,以便您可以看到保存在该提交中的内容。

未跟踪的文件 是您 work-tree 中但不在 Git 的索引中的任何文件。当你有这样一个文件——一个不在 Git 的索引中——并且 git checkout 其他一些提交也没有那个文件时,那个文件继续不在 Git' s 索引,因此继续未被跟踪。

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

摆脱提交

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

假设一些 b运行ch 名称和一些提交系列:

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

现在进一步假设我们可以说服 Git name master 应该持有,而不是提交 H 的哈希 ID,但是 commit G 相反,像这样:

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

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

(我们可以,当我们绘制这些时,将“丢弃的”提交向上或向下推以将它们移开。我有点受 Whosebug 文本约定的限制,但是如果您将它们绘制在纸上或一个白板,随意以任何方式绘制它们,包括从 b运行ch 名称到提交的长长的弯曲箭头。)

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

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

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

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

也就是说,现在看起来我们在 b运行ch develop 上提交了 H。尽管如此,b运行ch master 现在在提交 G 时结束,所以我们确实做了 something.

这就是 git reset 所做的。当你 运行:

git reset --mixed HEAD~

你告诉你的 Git:找到从 HEAD 退一步的提交——从当前 b运行ch 的最后一次提交退一步。然后,强制使用当前的 b运行ch 名称来标识该提交。 如果您有:

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

你这样做一次,你最终得到:

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

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

git reset--mixed 参数告诉 Git 在移动东西时 保持 work-tree 不变 。因此 work-tree 中的文件副本将被单独保留。使用 --hardgit reset 也会调整这些。使用 --softgit reset 单独保留 Git 的索引,但是使用 --mixed,Git 清空旧索引并从提交中填充它 select.

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

请记住,所有 committed 文件在这些提交中都是安全的,只要您仍然可以 find 那些提交。 通过像提交 I 这样的提交很难找到,你已经设置好了,所以你可能无法获得 those轻松恢复文件的版本。但是 git log 向您展示的任何提交,好吧,那些 提交很容易找到。 (我们跳过了使用 Git 的 detached HEAD 模式来查看历史提交的想法,这样就不必涵盖该模式,但仅此而已查看历史版本的方式。)

添加更多 Git 个存储库

现在你知道怎么做了ommits 和 b运行ch 名称,在 你的 存储库中工作——包括添加新提交,以及重置一些提交——是时候添加 GitHub Git加入组合。

要让您的 Git 调用其他 Git,您需要有一个 URL——类似于 ssh://git@github.com/...https://github.com/...。您的 Git 将为您 保存 这个 URL,使用一个简短易记的名字。 Git 称其为 远程 。许多 Git 存储库只有一个远程存储库,称为 origin,我假设您的存储库也是如此。

要让您的 Git 与另一个 Git 连接,您需要 运行 三个命令之一:git fetchgit pushgit pullgit pull 命令只是一个方便的包装器,首先是 运行s git fetch,然后是第二个 Git 命令,这是最好的——好吧,I 认为最好——单独学习 git fetch。所以这只给了我们两个让 Gits 互相交谈的命令。

两条命令本身比较简单:

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

    现在您已经拥有他们拥有的所有提交(加上您自己未共享的任何提交),您的 Git 创建或更新您的 origin/* 名称,至 记住他们的他们的b运行ch名字。您的每个 origin/* 名称都是一个 remote-tracking 名称7 这些只是您的 Git在你 运行 git fetch.

    时,他们在 b运行ch 名称中有什么哈希 ID 的记忆

    如果他们不改变他们的名字运行ch 名字(曾经,或经常),你的git fetch将设置你的remote-tracking 每次都正确命名。如果他们确实经常更改它们,您需要经常 运行 git fetch 以获取任何新名称和不同的提交哈希 ID。

    你可以 运行 git fetch 就像这样,根本没有参数。

  • git push 让你的 Git 调用他们的 Git 然后 给他们新的提交 如果需要的话。这比 git fetch 稍微复杂一点,因为他们 记住 任何新的提交,他们将不得不更新 他们的 b 运行ch 名称!他们没有等同于 remote-tracking 的名字。

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

    然后,一旦他们的 Git 有任何提交要求,您的 Git 发送一个或多个请求:请,如果没问题,请设置您的 b运行ch name ______(填一个名字)到______(填一个hash ID)。是否服从这种礼貌的要求,由他们自己决定。或者,你的Git可以发一个命令:将你的名字_____设置为_____!是否服从还是由他们决定。

    git push命令需要8,你把名字origin,因为很久以前,有人说语法是git push <em>remote b运行ch</em> 所以你必须先把 origin 放在那里b运行ch 名称。然后,git push 需要 b运行ch 的名称。这告诉你的 Git 要发送哪个提交——你的 Git 像往常一样从你的 b运行ch 名称中找到 last 提交——并且填补了两个空白。即我们必须将b运行ch namehash ID都填入两个空格中,无论是礼貌的请求还是强行的命令。你的 Git 从你在这里输入的名字中得到这两个。9

    他们告诉我们 Git 他们是否服从了命令。如果是这样,我们的 Git 更新我们的一个 remote-tracking 名称对应于他们的 b运行ch 名称。也就是说,如果我们让他们更新他们的 master,我们的 Git 会更新存储在 origin/master 中的内存。由于我们没有找到他们的任何 other b运行ch 名称,我们其他 origin/* 名称中的 none 得到更新。


7Git 调用这些东西 remote-tracking b运行ch names, 但我发现这里的单词 b运行ch 使事情变得更加混乱,而不是更少;所以现在我把它放在一边,只称它们为 remote-tracking names.

8ca 进行设置,使 git push 默认为 推送当前 b运行ch,然后你可以忽略它—Git 将计算出正确的遥控器和当前的 b运行ch——但我喜欢显示显式版本。

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


Fast-forwards 和 non-fast-forwards

让我们想象一下,现在,我们是一个正在接收 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,因为这个操作不仅 add 提交给我们的 master,它还丢弃 一个提交。

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

你自己的情况

我们可以画出你的情况,你的master是你的origin/masterahead 1, behind 2——你的Git对[=431=的记忆]他们的 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 与提交 [=] 相比的更改208=],进行新的提交。您可以使用 git rebase 复制 现有提交 K 到添加到提交 J 上的新的不同提交。您 可以 做很多事情。每个都有一组不同的结果提交。记住提交是什么以及为您做什么,以及 b运行ch 名称如何找到提交,并决定您想要哪些提交,哪些您希望假装从未发生过——用 git reset [= 删除811=] git push --force—然后按照您想要的方式在您自己的本地存储库中进行设置。然后使用 git push,有或没有 --force,将新提交发送到 GitHub Git,并让他们设置 他们的 b运行ch 指向新提交的名称,以匹配您在 your Git.

中的设置