0

我有两个项目,一个是另一个项目的镜像,我在无镜像项目中有一个分支,我需要移动到镜像项目。

我正在做下一个:

git remote add upstream https://github.com/my/nomirrorProject.git
git fetch upstream upstreamBranch:mylocalbranch

但我收到下一条错误消息:

fatal: Refusing to fetch into current branch refs/heads/myLocalBranch of non-bare repository

git push origin mylocalbranch

有任何想法吗?

谢谢!

4

1 回答 1

0

TL;博士

除非您确切知道自己在做什么,否则不要使用git fetch upstream upstreamBranch:mylocalbranch语法。同样,不要使用git fetch origin theirbranch:mybranch. 相反,使用git fetch upstream后跟以下之一:

  • git checkoutgit switch, 或
  • git merge, 或者
  • git rebase

取决于您的预期目标。

你在做什么

Git 是关于提交的。Git 与分支无关,尽管分支名称可以帮助您(和 Git)找到提交;Git 是关于提交的。Git 也不是关于文件的,尽管每个提交都包含文件。这意味着您首先需要知道提交是什么以及对您有什么作用,其次,Git存储库由多个数据库和一些额外的东西组成,以使它们对您有用。第一个(通常是迄今为止最大的)数据库保存提交和其他对象。

提交

在 Git 中提交:

  • 有编号。每个 Git 提交都有一个全局(跨越每个 Git 存储库,即使它与您的 Git 存储库无关)唯一 ID,Git 将其称为哈希 ID对象 ID (OID)。这就是两个 Git 存储库在街上(或网络上)相遇时如何决定它们是否有共同的提交:通过比较这些 ID。这些哈希 ID 非常大且丑陋;它们在人类看来是随机的,尽管它们根本不是随机的;人类基本上从不直接使用它们(这会让我们发疯)。

  • 保存快照和元数据:

    • 每个提交都有每个文件的完整快照——或者更准确地说,它听起来是多余的,它拥有的每个文件。听起来冗余的短语处理了一些提交添加了新文件,而一些后续提交可能删除了文件的事实。每次提交,一旦提交,就会一直被冻结,因此其保存的文件永远可用。

      提交中的文件以一种特殊的、只读的、仅限 Git 的格式存储,在该格式中它们被压缩和重复数据删除。因此,一个存档(提交)主要重用以前提交的文件这一事实意味着这些存档占用的空间非常小。事实上,如果你做出一个完全重用旧文件的新提交——这可能以多种方式发生——新提交根本不需要空间来保存文件,只需要一点空间来保存元数据。

    • 同时,每个提交都包含一些元数据。这也一直被冻结(散列方案取决于此)。元数据包括提交人的姓名和电子邮件地址等内容。它们包括一条日志消息,您可以在其中写下提交的原因。(不要只是说您更改了第 42 行或其他任何内容:Git 可以从快照中找出答案。说明您更改第 42 行的原因。之前有什么问题?程序表现出什么行为是不好的,就是现在通过此更改纠正?)

      在这个元数据中,Git 存储了一些Git需要的信息:特别是早期提交列表的原始哈希 ID。Git 将这些称为提交的父母

通常在这个元数据列表中只有一个哈希 ID。也就是说,大多数提交只有一个父级。这些是您的普通提交。

通过持有单个父项的哈希 ID,每个提交“指向”其前任。这会形成一个向后的提交链。例如,假设我们有一些我们将调用的带有一些哈希的提交H,并且我们用从它出来的箭头绘制它,表示这个指向其父提交的反向指针:

            <-H

较早的提交H点有一些其他不同的哈希 ID,但就像 一样H,存储快照和元数据,所以让我们将此提交绘制为提交G,并带有一个向后的箭头:

        <-G <-H

因此,提交G指向更早的提交。让我们称之为F

... <-F <-G <-H

F再次向后指向,依此类推。这存储库中的历史记录,从提交开始(结束?)到H工作(向后),一次提交一个。

换句话说,存储库中的历史只不过是存储库中的提交。每个提交都有每个文件的完整快照,按照您(或任何人)提交时文件的形式及时冻结。而且,每个提交都有一个唯一的编号;我们只是使用这些大写字母让我们微弱的人脑在这里管理它们。

请注意,在某个存储库中进行的第一次提交没有父级,因为它不能有父级。所以它只是没有箭头出来。我们可以通过这种方式绘制一个包含 8 个提交的完整链,然后:

A--B--C--D--E--F--G--H

提交H是最后一次提交,在历史的开始(结束?),A作为第一次提交,在历史的结束(开始?)。Git 向后工作,所以历史“从头开始”。

这是分支名称的来源

Git 需要一种快速的方法来查找最后一次提交。我们很容易看到这些简单绘图中的最后一个,但真正的存储库可能有数千或数百万次提交,并且您制作的任何绘图通常都会变得非常混乱(这取决于存储库)。因此,为了提供一种简单的方法来查找最后一次提交,Git 使用了一个分支名称,如下所示:

...--G--H   <-- main

名称仅包含 链中最后一次提交main的原始哈希 ID 。从这里开始,Git 将像往常一样向后工作。

如果我们想拥有多个分支名称,我们只需创建另一个名称,也指向 commit H,如下所示:

...--G--H   <-- develop, main

使用提交

虽然提交存储文件(快照)和元数据,但存储的文件是只读的,并且是一种格式——一个 Git 内部对象——首先只有 Git 可以读取。没有其他程序可以读取这些文件,也没有任何东西——甚至 Git 本身——可以覆盖它们。但这不是我们的计算机程序想要的工作方式。他们想要读写真实的文件,而不是奇怪的 Git 化的内部对象。

那么,要使用提交,Git 必须从快照中复制所有文件。这是做什么git checkout或做什么git switch<1 你选择一个你希望 Git 提取的提交,然后运行:

git switch develop

例如挑选出 commit H。Git 现在将文件提取到工作区,Git 将其称为您的工作树工作树,您可以在其中查看它们,如果您愿意,也可以更改它们。

请注意,这些是可以随意使用的文件。Git 没有使用它们。如果你告诉它,Git 最终会使用另一个区域将它们复制回来,以使它们为的提交做好准备,Git 称之为staging area,我们不会在这里正确描述。但现在这些是你的文件。如果您运行git checkoutgit switch再次运行,Git 可能会删除这些文件并放入其他文件。


1您可以使用任一命令;git switch是较新且功能较弱的,因此危险性较小。想想一把过于复杂的瑞士军刀:你想要那种有自启动电锯刀片的,还是只有普通刀片的?有时您可能需要电锯,但最好将其保留为单独的工具。


更新分支名称

现在让我们简要地看一下在运行时分支名称是如何更新的git commit。您已经运行git switch develop以选择提交H工作。Git 将特殊名称附加HEAD到名称develop以记住这是当前分支名称,如下所示:

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

您对各种文件进行更改并运行git add(出于我们跳过的原因),然后运行git commit​​. Git 准备一个新的提交,收集元数据——你的姓名和电子邮件地址、你的日志消息,以及为了 Git 的历史目的,提交的原始哈希ID——并制作所有H文件的新快照,考虑到添加的更新和/或新的和/或删除的文件。这些都一起进入一个新的提交,其父级是现有的提交。让我们把它画进去:IH

          I
         /
...--G--H

我已经画I了一条新线,并故意省略了名称。现在让我们把名字放回去。 Git 在这里做了一些非常鬼鬼祟祟的事情,因为名称develop-HEAD附加到 - 不再指向提交H

          I   <-- develop (HEAD)
         /
...--G--H   <-- main

如果我们添加另一个新的 commit J,我们会得到:

          I--J   <-- develop (HEAD)
         /
...--G--H   <-- main

请注意,现在有两个提交是 ondevelop而不是 on main。如果我们这样做git switch main,Git将从我们的工作树中删除J来自 commit 的所有文件,并将所有来自 commit 的文件放置到位H

          I--J   <-- develop
         /
...--G--H   <-- main (HEAD)

我们现在main再次“开启”,使用来自 commit 的文件H。最新的分支main提交是提交H,而最新的分支develop提交是提交J

现在让我们创建另一个新分支,命名为topic,然后切换到它。这也将指向 commit H

          I--J   <-- develop
         /
...--G--H   <-- main, topic (HEAD)

现在让我们更改一些文件git add、 和git commit. 这会创建一个新提交K,其父项是H(not I, not J, but H),因为H当前分支 name找到的当前提交。然后,在提交之后,Git 将提交的哈希 ID写入名称 topicKK topic

          I--J   <-- develop
         /
...--G--H   <-- main
         \
          K   <-- topic (HEAD)

这些是我们的分支:H最新的提交mainJ最新developK最新topic。历史从这里倒退,所以从K我们回到H,然后G,等等;从J我们回到I,然后H,然后G,以此类推;并且从H,我们回到G等等。

这也意味着所有的提交H都在所有三个分支上。 在 Git 中,提交通常在多个分支上。

分支名称不是 Git 中唯一的名称

除了分支名称,我们还可以有标签名称,例如。这两种名称之间的主要区别是:

  • 您无法“打开”标签名称:git checkout v1.2如果v1.2是标签名称,则会产生 Git 称为分离的 HEAD的内容,并git switch v1.2给您一个错误,除非您添加--detached以允许 Git 进入分离的 HEAD 模式。

  • 标签名称不会自动更新。这是您无法“打开”标签名称这一事实的结果。当您进行新的提交时,Git 会更新您所在的分支名称,并且在 detached-HEAD 模式下,您根本不在任何分支上。

  • 标记名称得到共享

为了解释最后一点,是时候谈谈克隆和git fetch.

克隆

我之前提到过,Git 存储库主要由两个数据库组成。一个数据库保存提交和其他内部对象,所有这些都由对象 ID 找到。(提交是四种内部 Git 对象类型之一,但要使用 Git,您通常不必知道这一点——与我在这里写的其他内容不同。)

另一个主数据库包含名称:分支名称、标签名称以及 Git 的所有其他名称。这些名称都包含对象 ID:主要是提交 ID(标签名称有时是一个值得注意的例外,但随后标签最终间接指向提交,因此您大多不必了解带注释的标签对象。我们将跳过在这里详细介绍,但稍后在您创建标签时会出现。

当您克隆Git 存储库时,使用:

git clone <url>

您正在指示您的 Git:

  1. 创建一个新的空目录(或使用现有的空目录)来创建一个新的空存储库;
  2. 添加一个 Git 称为远程的东西——一个包含 URL 的短名称,标准的第一个“远程”被命名——origin这样你的 Git 可以随时调用其他 Git 存储库;
  3. 现在调用另一个 Git 存储库,并获取他们所有的提交,但不要 - 完全 - 复制他们的分支名称;
  4. 而是重命名他们的分支名称,但(通常)获取他们所有的标签名称;和
  5. 在您的新克隆中创建一个分支名称。

所以你有你的 Git 软件复制他们的提交和其他对象数据库,但你没有让你的 Git 复制他们的分支名称。相反,您让 Git 获取它们的每个分支名称并将其转换为远程跟踪名称

远程跟踪名称本质上是2通过获取它们的分支名称(例如mainordevelopfeature/tall或其他名称)并将您自己的远程名称(此初始克隆的 <code>origin)粘贴到 get origin/mainorigin/developorigin/feature/tall等前面而形成的。您的 Git 使用它们的所有分支名称来执行此操作。您的 Git 不会对它们的标签名称执行此操作:如果它们有 av1.2和 a v2.0,您的 Git 也会创建自己的标签名称v1.2拼写v2.0

所以标签名称与分支名称的不同之处在于这种额外的方式:它们不仅不应该移动——它们应该永远标识一个特定的提交,而不是某个分支上的最新提交——而且它们也可以共享。分支名称不共享


2这掩盖了很多细节。


添加遥控器并使用git fetch

您可以拥有任意数量的遥控器。第一个通常称为origin,并git clone为您制作这个。实际上,基本上是六命令序列的简称,其中五个是 Git 命令:git clone url

  1. mkdir(或您的操作系统用来创建新的空目录的任何命令),所有 Git 命令都在新目录中运行;
  2. git init, 在这个新目录中创建一个空存储库;
  3. git remote add origin url, 添加origin为遥控器;
  4. 需要的任何额外git config命令(有时有几个);
  5. git fetch origin,获取所有提交并重命名分支;和
  6. git checkout/git switch使用“创建新分支”选项。

您的 Git 在步骤 6 中签出的分支是您通过命令-b选项选择的分支git clone。如果您不提供-b选项,您的 Git 会询问他们的 Git 软件他们的存储库推荐哪个分支名称。然后,您的 Git 使用该分支名称(您的 Git 重命名为)来创建您的分支,指向与的.origin/whateverwhateverorigin/whatever

如果他们推荐的名称是main,那么您可能会得到这样的结果:

          I--J   <-- origin/develop
         /
...--G--H   <-- main (HEAD), origin/main
         \
          K   <-- origin/topic

请注意,您如何为每个分支名称提供一个远程跟踪名称,以及您自己的一个分支名称。

现在,您可以根据需要运行git remote add upstream以添加名为upstream. 提供一个你的 Git 应该调用的 URL。然后运行:

git fetch upstream

没有参数,你的 Git 会调用那个 Git。他们将为您的 Git 列出所有分支名称以及与这些分支名称相关的提交哈希 ID。

由于您之前的git clone,您的 Git 可能已经拥有大部分(如果不是全部)这些提交,这些提交是通过您的origin/*远程跟踪名称找到的。对于他们有的任何提交,而没有,你的 Git 会要求他们的 Git 打包并发送这些提交。这可能包括一些额外的提交,或者不包括。在任何情况下,您的 Git 现在都采用它们的每个 ( upstream's)分支名称并将它们重命名为如下形式的名称upstream/main

          I--J   <-- origin/develop, upstream/develop
         /
...--G--H   <-- main (HEAD), origin/main, upstream/main
         \
          K   <-- origin/topic
           \
            L   <-- upstream/topic

在这里,它们的三个分支名称upstream与您在调用的 Git 上找到的 Git相同origin。但是名字是commit upstream,不是commit 。所以你的 Git从他们那里获得了提交。你的 Git 不需要获取任何其他提交——你已经有了其余的提交——然后你的 Git 创建了你的名字。topicLKLupstream/*

你在做什么git fetch upstream theirbranch:mybranch

上面,我描述了不使用任何额外参数时的正常操作。如果您确实使用了额外的参数,例如:git fetch remote

git fetch origin main

或者:

git fetch upstream main

之后的其余参数remote是 Git 所说的refspec

refspec 可能会变得复杂,但它有两种相对简单的形式。一种形式是这样的:只是一个分支或标签名称。如果 Git 可以做到,Git 会从上下文中判断它是分支名称还是标签名称;如果没有,你必须明确告诉 Git 这是一个分支或标签名称,我们不会在这里展示。

更复杂的形式有两个由冒号分隔的名称:

git fetch upstream main:upmain

左侧的名称是source,它git fetch是远程存储库的分支或标记名称。3 右侧的名称是目标:对于git fetch,这是您希望 Git 在存储库中创建或更新的分支或标记名称

此更新操作通过将新的哈希 ID 推入 name(如果名称存在)或通过创建包含哈希 ID 的分支或标记名称(如果名称尚不存在)来工作。

如果您在这样的main分支上:

          I--J   <-- origin/develop, upstream/develop
         /
...--G--H   <-- main (HEAD), origin/main, upstream/main
         \
          K   <-- origin/topic
           \
            L   <-- upstream/topic

那么您当前的分支main并且您当前的提交是 commit H

如果您要运行:

git fetch upstream topic:topic

这会告诉你的 Git 去upstream,发现他们有 commitL作为他们的topic,如果需要的话带来 commit L- 它不需要,因为你现在有它 - 然后创建或更新你的分支名称topic以指向 commit L。由于您没有分支名称topic,您的 Git 可以执行此操作,生成:

          I--J   <-- origin/develop, upstream/develop
         /
...--G--H   <-- main (HEAD), origin/main, upstream/main
         \
          K   <-- origin/topic
           \
            L   <-- topic, upstream/topic

请注意,您当前的分支 main继续指向 commit H

但是如果你要求你的 Git:

git fetch upstream topic:main

你现在告诉你的 Git 找到他们topic对 commit 的引用L,并将 commitL的哈希 ID 写入你的 name main。如果您的 Git确实这样做了,您将拥有:

          I--J   <-- origin/develop, upstream/develop
         /
...--G--H   <-- origin/main, upstream/main
         \
          K   <-- origin/topic
           \
            L   <-- main (HEAD), upstream/topic

这将表明您当前分支main的当前提交是L. 这里的问题是工作树(和索引)中的所有文件都来自 commitH,而不是 out commit L。它们仍然会匹配 commit 中的文件H

因此,您的 Git 说不,我不会将名称main移至 commit L,因为这会破坏您当前 checkout 的顺利工作。会的,所以不要那样做。赶紧跑:

git fetch upstream

然后,如果您真的希望您的名字main指向 commit L,请使用它git reset --hard upstream/topic来实现,确切地知道做什么git reset --hard4


3 Refspecs 也可以使用git push,尽管它们的解释在这里有点不同,并且git push源是您的存储库,而不是远程存储库。)

4请记住,这git reset --hard意味着如果我有未保存的工作,请将其不可恢复地销毁。吉特会这样做!您可能应该首先确保您没有未保存的工作。

于 2022-02-01T09:48:17.580 回答