首页 > 解决方案 > 这个分支提前 11 次提交,在 master 后面 1 次提交

问题描述

使用 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)

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

提前致谢

标签: gitgithubversion-control

解决方案


这通常根本不会发生。但最近它暂时发生了很多。这种混乱可能会持续一段时间,然后可能最终停止,或者变得更糟。这是发生了什么。有关背景,请参阅我更长的“提交和分支名称在 Git 中的工作方式”答案中的任意数量,例如这个.

Git 分支名称需要包含一些现有的有效提交的哈希 ID。1 但是当我们第一次创建一个新的、完全空的存储库时,没有提交。如果分支名称必须包含提交哈希 ID,并且没有提交,那么 Git 应该在分支名称中存储什么哈希 ID?

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

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

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

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


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


最近发生了什么事?

为什么我们现在看到大量包含两个或有时更多根提交的存储库?答案来自于回答另一个问题。

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

假设我们使用 GitHub 或 Bitbucket 之类的 Git-Repository-Storage-Site 来存储存储库。这些站点通常要求我们使用某种点击按钮的 Web 界面来创建新的存储库。2 进一步假设我们还在git init笔记本电脑上使用本地来创建新的空存储库,或者运行以将空存储库GitHub 或任何地方git clone复制我们的笔记本电脑。

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


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


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

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

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

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

  1. mkdir path:这将创建一个新的、完全空的目录,Git 可以在其中创建存储库。如果省略path参数,Git 将根据 URL 计算出一个。

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

  3. git remote add origin url. 这会将您提供的 URL 保存在标准名称下origin

  4. 保存您指定的git config配置所需的数量。(列出此步骤很重要,因为它可能会影响下一个步骤。特别是单分支克隆,此处未正确介绍,请进行一些特殊配置。)

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

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

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

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

  • 我们提供了一个-b论据,他们有这样一个分支;这就是我们所在的分支,也是我们签出的提交。(如果他们没有我们命名的分支,我们git clone会给我们一个错误并删除它创建的目录,因此它看起来甚至没有启动。)

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

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

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

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

这个问题正在修复中,但目前,即使是现代 Git 版本,我们的 Git 也没有从他们的 Git 中获取和使用名称。 因此,如果他们的Git 选择了main,而我们的Git 选择了master,那么在创建那个空存储库时,克隆过程会发生以下情况:

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

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

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

这是混乱的秘诀。它实际上并不会导致两个单独的根提交,但他们推荐了一个我们不使用的分支名称,我们正在使用他们不推荐的分支名称,此时。


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

4或者, to 的参数-b可以是一个标签名,如果成功的话,它将导致一个分离的 HEAD 克隆,但我们将再次忽略这种情况。


案例 2:两个非空初始存储库

我不完全确定如何称呼案例 2,但我见过人们这样做:

  • 他们git init在笔记本电脑上运行,在 branch 上创建一个空存储库master
  • 然后,他们在此笔记本电脑存储库中创建初始提交。
  • 同时,他们使用 GitHub(或任何站点)Web 界面来创建一个新的存储库,但选择创建一个非空存储库:一个包含模板来源的 LICENSE、README 和/或其他初始文件的提交。这将创建一个名为 的分支main

They now connect their laptop repository to their GitHub repository, using git remote add origin url, and run git push origin master or similar. They also run git fetch origin (before or after git push origin master).

They now have, in their laptop and GitHub repository, two initial commits, not related to each other, on two different branch names.

They don't understand that they have just done this. Git doesn't complain: this is actually a perfectly valid situation. The set of commits in a repository is not required to have only one root commit. The commit graph in a repository can contain multiple disjoint subgraphs.

Your situation looks like the latter

Suppose we use this method, of creating two non-empty repositories: one on GitHub with a main, and one on a laptop with a master.

If we never get around to pushing master, but do create a dev branch using the name master, our new dev branch on the laptop will be using this same initial commit. Note that we can also run:

git checkout -b dev

when we're in the unborn-master-branch state. The next commit we make will be a root commit.

There is a giveaway. When we make a new commit, Git prints messages:

$ 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

Note the (root-commit) in the output. This indicates that this git commit command created the current branch name: we were in the orphan/unborn state a moment ago.

Subsequent commits omit the (root-commit) part here. We know that these commits are adding on to whatever commits we have so far.

The only way to be sure about the commit graph, though, is to use something that shows the commit graph. See Pretty Git branch graphs (and note that --oneline has a subtle flaw where it can look like two separate graphs are conjoined, when they are not).


推荐阅读