2

我已使用此处概述的步骤成功更新了我的主分支。因此,fork 的 master 分支现在与原始源的 master 相同。

我有几个不同的分支,我想制作其中的一个(称为 new_branch),即使是原始来源的主人也是如此。因此,我通过以下方式修改了链接中概述的步骤。

git fetch upstream(链接中的第4步)

git checkout new_branch(第 5 步)

git merge upstream/new_branch(第 6 步)

Step 6merge: upstream/new_branch - not something we can merge在终端中产生

我仍然继续下一步。

git push origin new_branch(第 7 步)

在第 7 步之后,我得到的只是Everything up-to-date. 然而,github 分支 `new_branch' 仍然说它是在 fork 源之后的 41 次提交。

是否不可能使您的非 master 分支与 fork 的源代码保持同步?

*我跑去看看我有什么git fetchgit branch -r(虽然我之前确实跑过git fetch

在此处输入图像描述

4

1 回答 1

2

TL;博士

您需要重新调整的提交,通常使用upstream/master. 然后您可能需要使用git push --force-with-lease或类似来更新您originnew_branch,即您的origin/new_branch. 有关详细信息,请参阅长答案。

这一切都相当复杂,所以这里是一个胶囊摘要:

  • 这里涉及三个Git 存储库。我们可以称它们为 Repo-A、Repo-B 和 Repo-C。不过,其中两个有一个 URL github.com。为了在引用三个存储库中的每一个方面保持理智,让我们在您的计算机上使用从存储库中看到的它们的名称
    • (无名称):本地存储库,laptop当我们需要名称时我们会调用它;
    • origin: 可以直接写入的 GitHub 存储库;和
    • upstream: 一个你不能写的 GitHub 存储库,但是你可以使用 GitHub 的能力来生成一个“拉取请求”。
  • 每个存储库都有自己的分支,但所有存储库都共享提交——至少,他们从其他 Git 存储库中看到的提交。
  • 我们通过其哈希 ID找到一个提交。在某种意义上,哈希 ID(或Git 术语中的对象 ID )就是提交。但是这些东西对用户并不友好。
  • 因此,我们(人类)使用名称找到哈希 ID 。但是名称不会在存储库之间共享。最多他们被复制。共享(如果有)是更新某些名称的结果。所以,在一个重要的意义上,它们不是共享的。
  • 要在and之间 传输提交,无论我们喜欢什么方向,我们都可以使用and 。laptoporigingit fetchgit push
  • laptop要在和之间传输提交upstream,我们只能使用git fetch.

现在,请记住,我们通过某种名称找到提交,让我们注意有很多种名称。在您自己的存储库中laptop,您可以完全控制所有这些名称,因此您可以做任何您想做的事情。为了您自己的理智,您需要遵循某些模式。

在名为的存储库origin中,您对名称的控制要少得多,但我们将了解如何使用它git push来影响 分支名称。在命名的存储库中upstream,您基本上无法控制名称。

请注意,我在这里完全忽略了标签。他们使图片有点复杂,这已经很长了。

获取、推送和远程跟踪名称

现在让我们谈谈git fetchgit push。我将假设你知道提交是如何工作的以及 Git 如何找到提交,从分支中的最后一个提交开始并向后工作,当我们git checkout按名称进行一些分支(或使用 做同样的事情git switch)然后创建新的提交,Git 更新分支名称以保存提交的哈希 ID。新的提交指向分支最后一个提交,因此分支已被此操作自动扩展。这都用了分支这个词,这在使用 Git 时是相当模棱两可的,以一种相当随意和推定的方式,假设读者可以找出任何数量的不同甚至可能相互矛盾的分支定义中的哪一个可能适用。

不会在这里假设您知道远程跟踪名称,因为您最初的许多问题都取决于这些。那么让我们谈谈这些。让我们也定义remote,因为它涉及到。

远程只是另一个 Git 存储库的简称。我们已经看到在 中有两个遥控器laptop:名称origin指代您在 GitHub 上的 fork,名称upstream指代 GitHub 上的另一个存储库,您从该存储库创建了您的 fork。远程总是存储一个 URL,但它也充当这些远程跟踪名称的前缀。

远程跟踪名称——Git 将其称为远程跟踪分支名称,但看看分支这个词已经被滥用得有多严重;让我们先把这个糟糕的词放在一边——是您的 ( ) Git 根据在其他 Git 存储库中看到的某个分支laptop名称创建和更新的名称。

再次记住,一个分支名称——在任何 Git 存储库中都可以看到,无论是laptoporigin还是——包含一个提示提交upstream哈希 ID。因此,无论我们查看三个存储库中的哪一个,我们都有一些以最近的提交结尾的提交字符串:

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

其中是该分支上最近提交H的哈希 ID 。如果这一切都在您的本地笔记本电脑存储库中,您可以使用或其他一些提交查看器查看这些提交。git log

您的第一个目标是获得所有提交,这意味着git fetch

为了对提交大惊小怪,我们需要拥有它们。您可以完全控制笔记本电脑上的存储库,这就是我们希望拥有它们的地方。我们使用git fetch.

需要git fetch的是您要从中获取提交的存储库的名称。也就是说,我们将选择您的一个遥控器: originupstream. 您的 Git 将使用存储的 URL 在 GitHub 上调用 Git,并将其连接到其中一个存储库。

就像您本地的 Git 一样,您调用的 Git(实际上是相应的存储库,但我们假设每个 Git 都是从事此类工作的人)可以看到分支名称和提交哈希 ID。使用git fetch,您的 Git 会要求他们向您报告他们所有的分支名称和这些哈希 ID。因此,您的 Git 可以看到他们的 Git 的名称和 ID 对。

对于这些 ID 中的每一个,您的 Git 现在可以检查您是否有此提交。如果你这样做了,太好了!没有什么可做的了。但如果你不这样做,那么,你的 Git想要这个提交,他们的 Git 很乐意把它给你。他们的 Git 有义务为您提供该提交(父母)相关的任何提交,您的 Git 可以检查您是否有这些,并要求它们,或者说不,谢谢,已经有一个合适的。由于这对他们提供的每个分支名称都重复,1你最终让他们向你发送他们拥有的每一个提交,而你没有。在这个“获取所有提交”过程结束时,您现在拥有了他们的所有提交

不过,我们之前注意到,哈希 ID 对人类来说是可怕的。这就是为什么他们的Git 首先有他们的分支名称。如果您的Git 也可以引用它们的分支名称,那会很好,不是吗?这就是你的 Git 所做的:它复制它们的名称,但改变它们。你有你的分支名称,他们也有他们的。你的 Git 不敢用他们的信息覆盖你的分支名称。相反,您的 Git 会使用它们的分支名称并将它们转换为您的远程跟踪名称。

为此,您的 Git 只需将遥控器的名称和斜线放在他们的名字前面。2如果我们从 获取 Git, 它们master将成为您的,origin/master如果我们从 获取 Git,它们将成为您的。originupstream/masterupstream

一旦这一切完成,你的 Git 就会与他们的 Git断开连接。他们的 Git 现在可以更新他们的分支名称,如果他们这样做了,你的 Git 的远程跟踪名称就会过时。这确实发生了,一直都在发生;要修复它,您只需git fetch再次运行,origin更新您的origin/*姓名,并upstream更新您的upstream/*姓名。但是当您断开连接时,您仍然拥有所有提交,并且您的 Git 会记住他们的Git 分支在哪里。


1如果您设置了一个单分支克隆,您的 Git 只会询问他们有关该分支的信息,因此您不会得到所有信息。单分支克隆的重点是……嗯,通常是为了节省空间和时间,但它们本身并没有节省太多,所以在你真正熟悉 Git 之前,请避免使用单分支克隆.

2从技术上讲,它比这更复杂,如果您不小心命名了自己的分支origin/xyz或其他东西,这会有所帮助。但是——只是不要那样做,然后你就不需要进入这些技术细节了。


当您需要发送提交时,您需要git push

假设你跑了git fetch origingit fetch upstream,背靠背,现在有所有的提交。您的笔记本电脑正在调用的 Gitorigin由您控制。这意味着您可以任何新的提交发送到 origin.

但是,根据控制者upstream和他们所做的设置,您可能无法将内容直接发送到upstream. 我们稍后会担心这一点,因为您的下一个目标可能是更新- 您masteroriginGit 调用origin/master.

事实上,你已经这样做了:

我已经使用 [at https://medium.com/@sahoosunilkumar/how-to-update-a-fork-in-git-95a7daadc14e] 列出的步骤成功更新了我的主分支。因此,fork 的 master 分支现在与原始源的 master 相同。

不幸的是,那个链接上的东西只是一个食谱。它没有解释为什么列出了列出的命令,让你在这个 xkcd中处于 Cueball 的情况:

“不知道。只要记住这些shell命令......”

那么,让我们仔细看看git push。虽然git pushGit 与 的对立面一样接近git fetch,但它们并不完全相反。最大的区别在于,git push没有远程跟踪名称的概念

要使用git push,您通常会使用两个参数运行它:

  • 远程的名称:这提供了 URL,就像git fetch; 和
  • Git 称之为refspec

refspec是这里的难点。我们可以保持简单,因为分支名称用作 refspec。但我们也许不应该把它保持得这么简单,因为这忽略了一些非常重要的事情。

请注意,如果您在git push 没有这两个参数的情况下运行,效果取决于您使用的是 Git 1.x 还是 Git 2.x。在 Git 1.x 中,git push可能会尝试推送太多。在从 Git 2.0 开始的 Git 版本中,git push默认只推送当前分支,这通常更接近大多数用户想要的。

远程部分,例如originin git push origin,很简单。这和以前一样:我们正在挑选打电话给谁。push命令将向他们发送提交,这是明显的对应物git fetch,从他们那里得到提交。真正不同的是这个最终的参考规范3 因此,我们需要定义什么是 refspec,以及如何编写一个,用于git push.

正如我们已经提到的,最简单的 refspec 形式只是一个分支名称,例如masteror new_branch。For git push, 这是master:masteror的简写new_branch:new_branch

这里更复杂的形式在中间有一个冒号:master:master例如。冒号将某些提交的本地名称与您计划向另一个 Git 发出的请求分开。

现在让我们继续看看它是如何git push工作的:

  • 首先,你的 Git 像以前一样调用他们的 Git。

  • 但是现在,不是让他们列出他们的分支名称以便您可以获取他们拥有的所有提交而您没有,而是让您的 Git 为他们列出一个提交哈希 ID。该哈希 ID 来自您的参考规范的左侧。

  • 他们查看这个哈希 ID,看看他们是否有这个提交。如果他们这样做,那就太好了!如果没有,您的 Git 和他们的 Git 会进行与之前发生的相同类型的对话,以确定您的 Git 需要向他们发送哪些提交。您的 Git 将根据需要向他们发送导致此提交的所有提交。他们将那些保存在某个地方,4然后继续到最后一部分。

  • 对于最后一部分,你的 Git 现在会给他们礼貌的请求或有力的命令。这些请求或命令的形式为:

    • 请,如果可以,请将您的分支名称______(填写名称)设置为______(填写哈希ID)。 或者:
    • 将您的分支名称______设置为_______!或者:
    • 我认为您的分支名称 ______ 包含哈希 ID ______。如果是这样,请将其设置为______!

最后一个请求或命令的分支名称来自冒号的右侧。提交哈希 ID 来自左侧,就像提交发送一样。所以git push master:master带上你的主人——无论提交哈希 ID 是什么——并将该哈希 ID 发送到他们的 Git。他们的 Git 确定他们是否已经拥有该提交,并确保在需要时获得它。然后你的 Git 要求他们将他们的分支名称, master, 设置为那个哈希 ID。

这里有几件重要的事情需要注意:

  • 没有远程跟踪名称的概念。他们不会修改你说的分支名称!如果你说master,他们会设置他们的master。或者他们不会,因为...
  • 他们可以说不。对于礼貌的请求,他们会首先检查您的请求是否只是添加了新的提交,或者至少不会抛出任何内容。如果不是这样,他们会说不。您会将此视为“被拒绝(非快进)”。
  • 即便是强硬的命令,他们仍然可以说不。如果您拥有GitHub 存储库,他们通常会接受强制命令,但 GitHub 向 Git 添加了一堆控件,以便您可以让他们拒绝。而且,如果您拥有 GitHub 存储库,则应用更多控制。
  • 最后一种形式是一种条件命令:我认为;如果是这样,我命令。所以他们可以对那个人说不,错误的形式是:“你错了。” 因为这个答案很长,这部分我就不详细说了。

无论如何,如果他们好的,我已经按照您的要求/命令更改了分支名称或创建了一个新的分支名称,此时您的 Git 会说:啊哈,我现在应该更新我的远程跟踪姓名。origin因此,例如, 如果您已经说服更新他们的master您的Git 现在将以origin/master相同的方式更新您的。


3这里有很多历史。跳过所有的历史,我们留下一个疑惑:为什么我们在 push 中使用 refspecs,而不是在 fetch 中使用?所以让我们稍微填写一下。

从技术上讲,该git fetch命令也需要 refspecs。在遥控器和远程跟踪名称发明之前,人们有时需要(或至少非常想要)将 refspecs 与 fetch 一起使用。远程跟踪名称的发明消除了大部分需求,但是缺少远程跟踪名称push意味着我们仍然需要它们push

幸运的是,在 Git 普及之前就已经发明了遥控器和 refspecs。然而,错误的默认值git pushGit 1.7 和 1.8 中仍然存在,有些人仍然使用这些版本。(Git 2.0 与 Git 1.9 几乎同时出现,而 1.9 似乎没有被使用。)

一个单独的分支名称作为 refspec 对fetchvs有不同的含义push,但由于我们通常不会在运行时放入 refspecs git fetch,所以这里不必担心。

4推送操作的接收者将传入的提交填充到“隔离”位置,以防他们最终选择拒绝它们。旧版本的 Git 缺乏隔离技巧,但它对于 GitHub 等网站非常重要。


有时您想发送,但不允许发送git push

在这里,您需要发出拉取请求。这是一个 GitHub 功能:它不是 Git 的一部分。我们不会在这里介绍它;对此有现有的问题和答案。不过,值得一提的是,这利用了 GitHub “fork”记住源存储库的方式。它不使用远程跟踪名称,而是使用 GitHub 人员发明的一堆东西。

三角工作:你不一定想要一个master分支

在 Git 中,分支名称的目的是能够定位特定的提交。此外,给定分支名称,您可以将该名称与git checkout或一起使用git switch。这会将您的本地 Git 置于您所在的状态,正如git status将要说的那样,on该分支(on branch masteron branch develop其他)。一旦你处于这种状态,你所做的提交将推进这个分支名称。您可能会有一些以 hash 结尾的提交H

...--F--G--H   <-- new_branch (HEAD)

你做了一些工作并运行git commitpoof,你有一个新I的 parent提交H分支名称现在可以找到新的提交:

...--F--G--H--I   <-- new_branch (HEAD)

但是,您拥有masteron的原因laptop是因为您运行了:

git clone <github url for the repository you call origin>

当你这样做时,你的 Git 在 GitHub 上调用了 Git,并将该存储库中的提交复制一个新的存储库中。然后,您的(笔记本电脑)Git创建了您自己的本地分支名称,使其识别您的(本地)Git 调用的相同提交:masterorigin/master

...--F--G--H   <-- master (HEAD), origin/master

然后创建新的功能分支new_branch,指向 commit H

...--F--G--H   <-- master (HEAD), new_branch, origin/master

您签出new_branch以便HEAD附加到名称:

...--F--G--H   <-- master, new_branch (HEAD), origin/master

然后你做出新的承诺I

...--F--G--H   <-- master, origin/master
            \
             I   <-- new_branch (HEAD)

请注意,远程跟踪名称始终如何origin/master标识 commit H

现在过了一段时间。一堆的提交被添加到你分叉的存储库中。你运行,你在 GitHub 上 fork 的存储库的 URL 在哪里。然后你运行:git remote add upstream urlurl

git fetch upstream

这会让你的 Git 调用他们的 Git 并从他们那里获取新的提交:

             J--K--...--T   <-- upstream/master
            /
...--F--G--H   <-- master, origin/master
            \
             I   <-- new_feature (HEAD)

现在这就是你正在做的事情(嗯,已经做了):

  • 首先,将您的git checkout master附加HEAD到您的master.

  • 接下来,git merge upstream/masterGit 是否发现它可以将您的分支名称master向前移动以直接指向提交T(这是一件棘手的事情git merge;我们没有在这里介绍)。结果如下所示:

                 J--K--...--T   <-- master (HEAD), upstream/master
                /
    ...--F--G--H   <-- origin/master
                \
                 I   <-- new_feature
    

    请注意没有提交如何更改。没有新的提交。我们所做的只是移动一个标签,以便分支名称master现在指向 commit T

  • 最后,git push origin master最终将提交发送J-K-...-T您的GitHub 分支,然后要求他们将他们 master的指向设置为 commit T。由于这只是一个附加组件(他们仍然不知道提交I但不关心,因为提交只是J通过T附加组件),他们接受礼貌的请求,并且您的 Git 更新您的origin/master.

最后一步之后的结果是,在您的本地 ( laptop) Git 中:

             J--K--...--T   <-- master (HEAD), origin/master, upstream/master
            /
...--F--G--H
            \
             I   <-- new_feature

但是:假设我们完全删除名称master。我们将从与以前相同的起点开始,减去一个分支名称:

...--F--G--H   <-- origin/master
            \
             I   <-- new_branch (HEAD)

如果需要,我们会做git remote add,然后git fetch upstream如果需要(我们已经做了两个,但让我们假装我们需要)来获得:

             J--K--...--T   <-- upstream/master
            /
...--F--G--H   <-- origin/master
            \
             I   <-- new_feature (HEAD)

现在,我们将运行以下命令,而不是任何结帐:

git push origin upstream/master:master

左侧的名称定位我们要发送的提交。这是提交T:我们所调用的内容的最后一次提交upstream/master,也就是upstreamGit 所调用的内容master

右边master的名字 , 是我们要origin设置的名字。

与以前相同的提交流程origin。就像以前一样,他们现在拥有最多并包括T(但他们没有I)的提交。然后我们要求他们将他们master的指向设置为T,他们这样做了,我们更新了我们的origin/master

             J--K--...--T   <-- origin/master, upstream/master
            /
...--F--G--H
            \
             I   <-- new_feature (HEAD)

最终结果大多相同。虽然我们不必在git checkout master本地,所以我们仍然在new_feature.

变基

我们剩下的一个问题是我们的提交I作为其父级提交H我们无法改变这一点! 现有的提交I存在,并且有一个哈希 ID。该提交永远是一成不变的。但是我们可以做的是让我们的 Gitcommit 中的快照与 commitI中的快照进行比较H,以查看我们更改了什么,并在commit 之后进行与新提交相同的更改T

换句话说,Git 通过复制commit进行新的提交I。让我们称它为 new-and-improved I, I'(eye-prime),并把它画进去:

                          I'  <-- ???
                         /
             J--K--...--T   <-- origin/master, upstream/master
            /
...--F--G--H
            \
             I   <-- new_feature (HEAD)

这个复制一些提交的过程是 Git 中的一个挑选操作,您可以使用git cherry-pick. 但是,如果您连续有多个提交要复制,那么快速简便的方法是使用git rebase. 作为奖励功能,git rebase使用分支名称移动来跟进复制。请注意,在上图中,我们没有找到新副本的名称I'。Rebase 通过从我们复制的最后一个提交中剥离当前分支名称并使其指向最后一个副本来解决这个问题。在这种情况下,我们可以将其绘制为:

                          I'  <-- new_feature (HEAD)
                         /
             J--K--...--T   <-- origin/master, upstream/master
            /
...--F--G--H
            \
             I   [abandoned]

我们确实失去了找到 commit 的哈希 ID 的简单方法I,但那是因为我们希望Git放弃的提交,转而支持新的和改进的提交。

这就是rebase的本质。虽然我不会详细介绍(因为又是这么长),要实现这种操作,我们通常只需运行:

git rebase <target>

在这种情况下,目标是提交T,我们可以使用名称找到它upstream/master。如果我们更新了origin'smaster以便我们也可以origin/master定位提交T,我们可以使用git rebase origin/master,但git rebase upstream/master会正常工作。

如果我们保留了master分支名称并对其进行了更新,我们也可以使用它git rebase master。关键是我们需要告诉git rebase去定位 commit T。 找到提交的任何T名称都可以,在这里。事实上,T如果我们想用鼠标剪切和粘贴它,我们甚至可以使用 's hash ID。

现在,如果您之前运行git push origin new_branch ,那么您已经Iorigin. 如果您现在尝试发送 commitI'origin让 Git将他们的名字origin指向commit ,他们会拒绝new_branchI'

他们拒绝的原因是这个礼貌的请求是要求他们放弃一些原始提交,以支持他们新的和改进的替代品。 当您运行git rebase. 但是现在你需要让他们的Gitorigin来做同样的事情。 这意味着您必须使用强制推送操作。

您可能已经读过强制推送是不好的。他们。但是我们面临两个糟糕的选择:您可以将拉取请求抛在后面,或者您可以放弃原始提交,转而使用这些新的和据称经过改进的替代品。如果放弃原始文件,您需要说服将这些提交复制到其 Git 存储库中的其他所有人也这样做。

有些人喜欢将拉取请求抛在脑后。这也没有什么根本上的错误。但是,如果您的公司或您的个人偏好要求您重新设置基准,执行此操作,然后使用git push --forceor git push --force-with-lease。(要找出这两者之间的确切区别,请搜索 StackOverflow。)

于 2021-04-26T08:50:21.657 回答