TL;博士
除非您确切知道自己在做什么,否则不要使用git fetch upstream upstreamBranch:mylocalbranch语法。同样,不要使用git fetch origin theirbranch:mybranch. 相反,使用git fetch upstream后跟以下之一:
git checkout或git switch, 或
git merge, 或者
git rebase
取决于您的预期目标。
你在做什么
Git 是关于提交的。Git 与分支无关,尽管分支名称可以帮助您(和 Git)找到提交;Git 是关于提交的。Git 也不是关于文件的,尽管每个提交都包含文件。这意味着您首先需要知道提交是什么以及对您有什么作用,其次,Git存储库由多个数据库和一些额外的东西组成,以使它们对您有用。第一个(通常是迄今为止最大的)数据库保存提交和其他对象。
提交
在 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 checkout或git 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是最新的提交main,J是最新的develop,K是最新的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:
- 创建一个新的空目录(或使用现有的空目录)来创建一个新的空存储库;
- 添加一个 Git 称为远程的东西——一个包含 URL 的短名称,标准的第一个“远程”被命名——
origin这样你的 Git 可以随时调用其他 Git 存储库;
- 现在调用另一个 Git 存储库,并获取他们所有的提交,但不要 - 完全 - 复制他们的分支名称;
- 而是重命名他们的分支名称,但(通常)获取他们所有的标签名称;和
- 在您的新克隆中创建一个分支名称。
所以你有你的 Git 软件复制他们的提交和其他对象数据库,但你没有让你的 Git 复制他们的分支名称。相反,您让 Git 获取它们的每个分支名称并将其转换为远程跟踪名称。
远程跟踪名称本质上是2通过获取它们的分支名称(例如mainordevelop或feature/tall或其他名称)并将您自己的远程名称(此初始克隆的 <code>origin)粘贴到 get origin/main、origin/develop、origin/feature/tall等前面而形成的。您的 Git 使用它们的所有分支名称来执行此操作。您的 Git 不会对它们的标签名称执行此操作:如果它们有 av1.2和 a v2.0,您的 Git 也会创建自己的标签名称v1.2拼写v2.0。
所以标签名称与分支名称的不同之处在于这种额外的方式:它们不仅不应该移动——它们应该永远标识一个特定的提交,而不是某个分支上的最新提交——而且它们也可以共享。分支名称不共享。
2这掩盖了很多细节。
添加遥控器并使用git fetch
您可以拥有任意数量的遥控器。第一个通常称为origin,并git clone为您制作这个。实际上,基本上是六命令序列的简称,其中五个是 Git 命令:git clone url
mkdir(或您的操作系统用来创建新的空目录的任何命令),所有 Git 命令都在新目录中运行;
git init, 在这个新目录中创建一个空存储库;
git remote add origin url, 添加origin为遥控器;
- 需要的任何额外
git config命令(有时有几个);
git fetch origin,获取所有提交并重命名分支;和
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 --hard。4
3 Refspecs 也可以使用git push,尽管它们的解释在这里有点不同,并且git push源是您的存储库,而不是远程存储库。)
4请记住,这git reset --hard意味着如果我有未保存的工作,请将其不可恢复地销毁。吉特会这样做!您可能应该首先确保您没有未保存的工作。