该分支提前 11 次提交,落后 1 次提交

This branch is 11 commits ahead, 1 commit behind master

正在使用 Git 作为我的 VCS 并使用 GitHub 作为远程仓库的项目开发分支。 我想做一个 PR,但我注意到以下消息,“这个分支提前 11 次提交,落后 1 次提交。”

经过一些调查,我注意到 main 和 dev 分支都由 2 个不同的用户进行了不同的初始提交。

总支:

commit 49038002fbb1d6cd1434f30809bcexxxxxxx (HEAD -> main, upstream/main, origin/main, origin/HEAD)
Author: John doeh
Date:   Wed Jul 28 14:41:30 2021 -0400

    Initial commit
(END)

开发分支

commit 5b39f1b9cd01d858b30bd5fd6770694xxxxxx
Author: Jane Doe
Date:   Fri Jul 23 18:00:00 2021 -0400

    Initial commit from Create Next App
(END)

我应该如何处理这个问题来解决它,并给出解释,以便我以后不会犯同样的错误?

提前致谢

通常根本不会发生。但最近 暂时 经常发生。这种混乱可能会持续一段时间,然后可能最终停止,或者变得更糟。这是怎么回事。对于背景,请参阅我较长的“提交和分支名称如何在 Git 中工作”的答案,例如 .

Git 分支名称 需要 包含一些现有的有效提交的哈希 ID。1 但是当我们首先创建一个新的 totally-empty 存储库,那里 没有提交 。如果分支名称必须包含提交哈希 ID,并且没有提交,那么分支名称中应该 Git 存储什么哈希 ID?

Git 对此的回答是,在新的 totally-empty 存储库中, 不能有任何分支 。这是一个简单而巧妙的答案,但它有一个很大的缺陷,因为 Git 还有一个要求:特殊名称 HEAD 应该包含一个分支的名称(如果你在一个分支上),或一些现有的有效提交的原始哈希 ID(如果您处于“分离的 HEAD”模式)。我们刚刚说过没有提交,所以你不能处于分离的 HEAD 模式。因此 HEAD 需要包含一个分支的名称——然而,我们刚刚说过不能有任何 分支

幸运的是,这里没有实际的矛盾,没有需要某种魔法状态或其他东西的悖论。要求是 HEAD 包含分支的 名称 ,而不是 现有 分支的 名称 Git 只允许 HEAD 包含一个不存在的分支的名称,巧妙地回避了悖论。

当你处于这种状态——在一个不存在的分支上——Git有时会说你在未出生的分支,有时会说你是在 孤儿分支 上。 Git 关于它使用的短语并不一致,但它通常使用这两个短语之一,或者它们的一些变体。

此外,当您处于此状态时,您所做的下一次提交 将自动成为root 提交。根提交只是没有 parents 的提交:初始提交。现在已经创建了根提交,Git 立即将生成的哈希 ID 存储到分支名称中,从而也创建了分支,并且解决了奇怪的状态。您的存储库有一个初始提交,现在可以有无限数量的分支名称,所有分支名称都必须 select 这一个初始提交。


1“现有”和“有效”是多余的;我在这里使用它们作为一种强调。这个想法是 Git 只允许任何给定的分支名称包含某些提交的哈希 ID,git cat-file -t <hash> 会说 commit 并且 git cat-file -p <hash> 会向您显示内部提交数据。


最近怎么样了?

为什么我们现在看到大量存储库有两个,有时甚至更多,root 提交?答案来自于回答另一个问题。

当我们创建一个新的空存储库时,HEAD 应该包含什么不存在的分支名称?这取决于 git init 命令。过去,git init 总是 创建一个新的空存储库,其中存储库位于名为 master.

的不存在的分支上

假设我们使用像 GitHub 或 Bitbucket 这样的 Git-Repository-Storage-Site 来存储存储库。这些站点通常要求我们使用某种 clicky-button 网络界面来创建新的存储库。2 进一步假设我们也在本地使用 git init,在我们的笔记本电脑上,创建新的空存储库——或 运行 git clone 将空存储库 GitHub 或任何地方 复制到 我们的笔记本电脑。

我们现在必须检查多个案例。


2GitHub,至少,终于,终于,提供了一个命令行脚本,gh,可以做这样的事情无需单击烦人的 Web 界面按钮。当然,如果您喜欢点击 Web 界面按钮,也许它们并不烦人。


案例 1:克隆一个空存储库

git clone的一般形式是,或者至少可以描述为:3

git clone [ -b <branch> ] [ -c config-options ] <url> [ <path> ]

这实际上是 shorthand 以下六个操作,其中五个是 Git 命令;五个 Git 命令在新目录中是 运行:

  1. mkdir <em>path</em>: 这将创建一个新的 totally-empty 目录,其中 Git 可以创建一个存储库。如果省略 path 参数,Git 将根据 URL.

    计算出一个
  2. git init:因为这是 运行 在一个新的、完全空的目录中,它创建了一个空的存储库:没有提交,因此必然没有分支。我们到底在哪个分支?我第 6 步顺利,没关系,但让我们继续。

  3. git远程添加源<em>url</em>。这会将您提供的 URL 保存在标准名称 origin.

  4. 保存您指定的配置所需的 git config 数量。 (列出这一步很重要,因为它可能会影响下一步。特别是,single-branch 克隆,此处未正确涵盖,请进行一些特殊配置。)

  5. git fetch origin:这会调用一些其他 Git软件,在存储的URL。我们现在从另一个 Git 存储库中获取所有 他们的 提交。 fetch 命令导致另一个 Git 也列出它们所有的 分支名称 ,我们的 Git renames将它们变成 remote-tracking names.

  6. 最后,我们的 Git 将我们的 -b 参数用于 运行 git checkoutgit switch。如果我们提供 -b 参数,它必须是存在于另一个存储库中的分支的名称。4 这就是我们的 Git 将 在本地创建,然后使用通过查看 their Git 的分支名称找到的提交哈希 ID 进行检出。也就是说,我们的 Git 查看我们的 remote-tracking 名称,这些名称基于它们的分支名称,并选择正确的重命名分支名称以获得正确的提交哈希 ID 来检查我们的分支。

    如果我们省略 -b 标志,我们的 Git 应该询问他们的 Git 他们推荐哪个分支名称。这部分在 Git 的当前版本中被巧妙地破坏了。 推荐的名称很简单,就是存储在 HEAD 中的名称。如果您使用 Web 界面更改某些 GitHub 存储库中的 默认分支 ,您真正要做的是让 GitHub 填充不同的名称该克隆中的 HEAD

在我们查看“克隆一个空存储库”案例之前,让我们考虑克隆一个更典型的 空存储库。这里有两种可能:

  • 我们提供了一个-b的说法,他们有这样一个分支;那是我们所在的分支,也是我们检出的提交。 (如果他们没有我们命名的分支,我们的 git clone 会给我们一个错误并删除它创建的目录,这样它看起来就像什么都没有启动。)

  • 我们未能提供 -b 论据,但他们推荐了他们的标准分支名称——不管是什么——这就是我们登陆的分支。

因此,由于第 6 步,我们在 HEAD 中获得了一些有效的分支名称,一切都很好。我们正在处理一些现有的提交;已经有一个初始(root)提交,我们将像往常一样添加新提交。

但请稍等:我们正在克隆一个完全空的 存储库。它没有分支名称。他们推荐什么分支名称? 他们能推荐一个分支名称吗?

最后一个问题的答案——他们可以他们推荐一个分支名称,即使他们没有任何分支名称——是并且一直是“是的,他们可以”大约十年。这意味着仍有一些人使用 Git 的旧版本 不能 ,因此不使用。幸运的是,托管网站并没有那么陈旧和破旧,所以他们可以推荐一个名字。但现在我们遇到了我提到的那个“微妙地破坏”的部分。

这正在修复,但 目前,即使使用现代 Git 版本,我们的 Git 也不会从他们的 Git. 因此,如果 他们的 Git 选择 main,而 我们的 Git 选择 master,在创建空存储库时,克隆过程会发生以下情况:

  • 我们(本地)执行步骤 1-5 就好了。
  • 我们然后(本地)失败执行第 6 步。我们的本地存储库保留在 我们 使用的任何初始分支名称上,而 他们的(GitHub 的,或 Bitbucket 的,或任何人的)存储库保留在 他们 使用的任何初始分支名称上。他们也继续推荐这个名字,因为它还在他们的 HEAD.

如果“他们”是 GitHub,他们现在使用 main 作为他们的标准第一个分支名称,但我们使用的 Git 版本使用 master作为我们标准的第一个分支名称,我们和他们都有不同的“未出生分支”。

我们现在进行第一次提交。这 创建了我们的分支 master,我们将其与 git push 一起使用。这 在网站上的存储库中创建了名称 master — GitHub 或其他 — 但 仍然让他们推荐 main.

这是一种混淆的方法。它不启动ly 导致 两个单独的根提交,但他们推荐了一个我们不使用的分支名称,而我们使用的是他们不推荐的分支名称,此时。


3命令提供了很多其他的选项,比如-o--no-checkout等等,可以稍微乱一点,但是总体计划保持不变。对我们来说最大的一个是 --no-checkout,它完全省略了第 6 步。

4或者,-b 的参数可以是一个 标签名 ,如果成功的话,结果将是在 detached-HEAD 克隆中,但我们将再次忽略这种情况。


案例 2:两个 non-empty 初始存储库

我不太确定如何称呼案例 2,但我看到有人这样做:

  • 他们 运行 git init 在他们的笔记本电脑上,在分支 master.
  • 上创建了一个空存储库
  • 然后他们在此笔记本电脑存储库中创建初始提交。
  • 与此同时,他们使用 GitHub(或任何站点)Web 界面创建新存储库,但选择创建 non-empty 存储库: 一次提交包含 template-sourced LICENSE、README、and/or 其他初始文件。这将创建一个名为 main.
  • 的分支

他们现在使用 git 远程添加来源 <em>url</em> 将笔记本电脑存储库连接到 GitHub 存储库 和 运行 git push origin master 或类似的。他们还 运行 git fetch origin(在 git push origin master 之前或之后)。

他们现在在他们的笔记本电脑和 GitHub 存储库中,两个初始提交,彼此不相关,在两个不同的分支名称上。

他们不明白他们刚刚做了这件事。 Git 没有抱怨:这实际上是一个完全有效的情况。存储库中的一组提交不需要只有 one 根提交。存储库中的提交图可以包含多个不相交的子图。

你的情况应该是后者

假设我们使用这种方法创建两个 non-empty 存储库:一个在 GitHub 上 main,另一个在笔记本电脑上 master

如果我们从来没有抽出时间推动 master,但确实创建了一个 dev 分支 使用名称 master,我们的新 dev 笔记本电脑上的分支将使用相同的初始提交。请注意,我们还可以 运行:

git checkout -b dev

当我们处于未出生-master-分支状态时。我们所做的 下一个 提交将是根提交。

有赠品。当我们进行新的提交时,Git 打印消息:

$ mkdir empty && cd empty
$ git init
Initialized empty Git repository in ...
$ echo root > README
$ git add README 
$ git commit -m initial
[master (root-commit) ea0681d] initial
 1 file changed, 1 insertion(+)
 create mode 100644 README

注意输出中的 (root-commit)。这表明这个git commit命令创建了当前分支名称:我们刚才处于orphan/unborn状态。

后续提交省略此处的 (root-commit) 部分。我们知道这些提交 添加 到我们目前的任何提交。

不过,唯一确定提交图的方法是使用显示提交图的东西。请参阅 Pretty Git branch graphs(并注意 --oneline 有一个微妙的缺陷,它看起来像是两个单独的图形是连在一起的,而实际上它们不是)。

固定

正如 torek 所解释的,我的问题属于情况 2。

克隆原始存储库后忘记正确同步本地和远程存储库,导致 2 个不同的“初始”提交,使两个分支的历史记录无关。

我使用 git rebase 解决了它,按照说明