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
文件的新快照,考虑到添加的更新和/或新的和/或删除的文件。这些都一起进入一个新的提交,其父级是现有的提交。让我们把它画进去:I
H
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写入名称: topic
K
K
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通过获取它们的分支名称(例如main
ordevelop
或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/whatever
whatever
origin/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 创建了你的名字。topic
L
K
L
upstream/*
你在做什么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
意味着如果我有未保存的工作,请将其不可恢复地销毁。吉特会这样做!您可能应该首先确保您没有未保存的工作。