从另一个 repo 获取上游,然后将更改推送到我的本地分支

Fetch upstream from another repo and then push the changes in my local branch

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

我正在做下一个:

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

有什么想法吗?

谢谢!

TL;DR

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

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

取决于您的预期目标。

你在做什么

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

提交

提交,在 Git:

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

  • 保留快照和元数据:

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

      提交中的文件以特殊的 read-only、Git-only 压缩格式和 de-duplicated 存储。因此,一个存档(提交)主要是 re-uses 个来自先前提交的文件这一事实意味着这些存档只占用很少 space。事实上,如果你做了一个 completely re-uses 旧文件的新提交——这可以通过多种方式发生——新提交根本不需要 space保存文件,只需 space 保存元数据。

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

      在这个元数据中,Git 存储了一些 Git 需要的信息:具体来说,早期提交列表的原始哈希 ID . Git 将这些称为提交的 parents

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

通过保存单个 parent 的哈希 ID,每个提交都“指向”其前身。这形成了一个向后的提交链。例如,假设我们有一些提交和一些我们称之为 H 的散列,我们用一个箭头来绘制它,表示这个指向其 parent 提交的向后指针:

            <-H

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

        <-G <-H

提交 G 因此指向 still-earlier 提交。我们称它为 F:

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

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

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

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

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

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

这就是分支名称的来源

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

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

名称 main 仅包含链中最后一次提交的原始哈希 ID。从这里开始,Git 将照常倒退。

如果我们想要有多个分支名称,我们只需创建另一个名称,也指向提交H,像这样:

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

使用提交

在提交存储文件(快照)和元数据时,存储的文件是 read-only,并且采用一种格式——Git 内部 object——只有 Git可以先读。没有其他程序可以读取这些文件,也没有任何东西——甚至 Git 本身——都不能 覆盖 它们。但这不是我们的计算机程序想要的工作方式。他们想读写真正的文件,而不是怪异的Git-ized内部objects.

使用 提交,然后,Git 必须从快照 中复制所有文件。这就是 git checkoutgit switch 所做的。<1 你选择一个 commit 你想要 Git提取,然后 运行:

git switch develop

例如选择提交 H。 Git 现在将文件提取到工作区中,Git 调用您的 工作树 work-tree ,在那里你可以看到它们,如果你愿意,也可以改变它们。

请注意,这些是您的 文件,您可以随心所欲地使用它们。 Git 没有使用它们。 Git 将,如果你告诉它,最终将它们复制回来,使它们为 new 提交做好准备,使用另一个区域 Git 调用 暂存区,这里就不一一赘述了。但现在这些是 你的 文件。如果您再次 运行 git checkoutgit switch,Git 可能 删除 这些文件并放入 other 个文件。


1您可以使用任一命令; git switch 是较新的,less-powerful,因此是 less-dangerous。想想一把过于复杂的瑞士军刀:你想要一把 self-starting 电锯 blade 的,还是只有一把普通刀 blade 的?有时您可能需要电锯,但最好将其作为单独的工具使用。


更新分支名称

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

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

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

          I
         /
...--G--H

我在新的一行上画了 I,并故意省略了名字。现在让我们把名字放回去。 Git 在这里 做了一些非常偷偷摸摸的事情,因为名字 develop——那个 HEAD 被附加到——不再指向提交 H!

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

如果我们添加另一个新提交 J,我们得到:

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

请注意,现在有两个 develop 上的提交不在 main 上。如果我们 git switch main,Git 将从我们的工作树中 删除 来自提交 J 的所有文件,并将来自提交的所有文件放在适当的位置H:

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

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

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

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

现在让我们更改一些文件,git addgit commit。这会生成一个新提交 K,其 parent 是 H(不是 I,不是 J,而是H),因为 H 当前提交 ,由 当前分支名称 topic 找到。然后,提交 K,Git 将提交 K 的哈希 ID 写入 name topic:

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

这些是我们的分支:H最新的 main 提交,J最新的develop 上,K 最新的 topic 上。历史从这里开始倒退,所以我们从 K 回到 H,然后是 G,依此类推;从 J 我们回到 I,然后是 H,然后是 G,依此类推;从 H,我们回到 G 等等。

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

分支名称不是 Git

中唯一的名称类型

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

  • 您不能“在”标签名称上:git checkout v1.2,如果 v1.2 是标签名称,则生成 Git 所称的 detached HEAD,并且 git switch v1.2 给你一个错误,除非你添加 --detached 允许 Git 进入 detached-HEAD 模式。

  • 标签名称不会自动更新。这是您无法“使用”标签名称这一事实的产物。当你进行新的提交时,Git 更新你所在的 branch 名称,在 detached-HEAD 模式下,你在 no 分支。

  • 标签名称得到共享

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

克隆

我之前提到过 Git 存储库主要由两个数据库组成。一个数据库保存提交和其他内部 objects,全部由 object ID 找到。 (提交是四种内部 Git object 类型之一,尽管要使用 Git 你通常不需要知道这一点——这与我在这里写的其他内容不同。)

另一个主数据库保存名称:分支名称、标记名称和所有 Git 的其他名称。这些名称都包含 object ID:主要是提交 ID(标签名称有时是一个值得注意的例外,但标签最终会间接指向提交,因此您几乎不必了解 带注释的标签 objects。我们将在此处跳过此细节,但稍后在您创建标签时会突然出现。

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

git clone <url>

您正在指示您的 Git 来:

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

所以你有你的 Git 软件复制他们的 提交和其他 objects 数据库,但你没有 Git复制他们的 分支名称 。相反,您 Git 获取他们的每个 分支 名称并将它们变成 remote-tracking 名称.

A remote-tracking name 本质上是 2 通过 branch名称,例如 maindevelopfeature/tall 或其他名称,并为这个初始克隆粘贴您自己的 remote 名称-origin — 在前面得到 origin/mainorigin/developorigin/feature/tall 等。您的 Git 对所有 分支机构 名称执行此操作。您的 Git 不会对他们的标签名称执行此操作:如果他们有一个 v1.2 和一个 v2.0,您的 Git 将创建您自己的标签名称拼写 v1.2v2.0 也是。

所以 tag 名称与 branch 名称的不同之处在于:它们不仅不应该移动——它们应该识别一个特定的提交永远,而不是某个分支上的最新提交——但它们也会共享。分支名称未 共享 .


2这掩盖了很多细节。


添加遥控器并使用 git fetch

您可以拥有任意数量的遥控器。 第一个通常称为origingit clone为您制作了这个。其实 git clone <em>url</em> 基本上就是a的缩写six-command序列,其中五个是Git命令:

  1. mkdir(或您的 OS 用于创建新空目录的任何命令),所有 Git 命令都在新目录中 运行;
  2. git init,在此新目录中创建一个空存储库;
  3. git远程添加origin <em>url</em>,添加origin作为远程;
  4. 需要任何额外的 git config 命令(有时会有一些);
  5. git fetch origin,获取所有提交并重命名分支;和
  6. git checkout / git switch 使用“创建新分支”选项。

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

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

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

请注意,您如何为他们的每个 分支机构 名称创建一个 remote-tracking 名称,再加上一个您自己的分支机构名称.

如果愿意,您现在可以 运行 git remote add upstream 添加一个名为 upstream 的遥控器。给一个 URL 你的 Git 应该调用的。然后运行:

git fetch upstream

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

由于您之前的 git clone,您的 Git 可能已经拥有大部分(如果不是全部的话)这些提交,这些提交是通过您的 origin/* remote-tracking 名称找到的。对于他们 有而你没有的任何提交,你的 Git 将要求他们的 Git 打包并发送这些提交。这可能包括一些额外的提交,也可能不包括。在任何情况下,您的 Git 现在都使用他们的每个 upstream 分支 名称并 将它们重命名 为形成像 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。但是 upstreamtopic 名称提交 L,而不是提交 K。所以你的 Git 从他们那里获得了提交 L 。您的 Git 不需要获得任何其他提交——您已经完成了其余的提交——然后您的 Git 创建了您的 upstream/* 名称。

你用 git fetch upstream theirbranch:mybranch

做什么

上面我描述的是git fetch <em>remote</em>在不使用任何额外参数的情况下的正常操作.如果你使用额外的参数,例如:

git fetch origin main

或:

git fetch upstream main

remote 之后的剩余参数是 Git 调用的 refspec.

refspec 可能会变得复杂,但它有两种相对简单的形式。一种形式是这样的:只是一个分支或标签名称。 Git 将根据上下文判断它是分支名称还是标签名称,如果 Git 可以做到这一点;如果不是,您必须通过明确告诉 Git 这是分支或标记名称来帮助 Git,我们不会在此处显示。

比较复杂的形式有两个名字用冒号:字符分隔:

git fetch upstream main:upmain

左边的名称是 sourcegit fetch 是远程存储库的分支或标签名称。3 右侧的名称是 destination: for git fetch,这是您希望 Git 到 create 的分支或标签名称或在你的 资料库中更新

此更新操作通过将新的哈希 ID 推入名称,如果名称存在,或者通过创建包含哈希 ID 的分支或标签名称,如果名称尚不存在。

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

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

那么你的当前分支main并且你的当前提交是提交H.

如果你要 运行:

git fetch upstream topic:topic

这会告诉你的 Git 转到 upstream,发现他们已经提交 L 作为他们的 topic,把提交 L如果需要——不需要,因为你现在有了——然后创建或更新你的分支名称topic指向提交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 继续指向提交 H

但是如果你问你的 Git 到:

git fetch upstream topic:main

你现在告诉你的 Git 发现他们有他们的 topic 引用提交 L,并将提交 L 的哈希 ID 写入你的名字 main。如果你的Git 做了 这样做,你会:

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

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

中的文件

您的 Git 因此说 不,我不会将名称 main 移至提交 L,因为这会破坏您当前的顺畅工作结帐。它会的,所以不要那样做。只是 运行:

git fetch upstream

然后,如果你真的想要你的名字main指向提交L,使用git reset --hard upstream/topic来实现,知道git reset --hard 的作用。4


3Refspecs 也与 git push 一起使用,尽管它们的解释在这里有点不同,对于 git push,来源是 你的 仓库,不是远程仓库。)

4记住git reset --hard的意思是如果我有未保存的工作,将其销毁不可恢复。 Git 会做到的!您可能应该首先确保没有未保存的工作。