你对分支名称和使用`git pull`存在一些误解。让我把它分成几个部分,并给您这个执行概述概述来开始:
- `push` 的对应项不是 `pull`,而是 `fetch`;
- `git pull` 只运行 `git fetch`,然后是第二个 Git 命令,通常是 `git merge`,我认为最好新 Git 用户避免使用 `git pull`,改用两个单独的命令;
- 虽然 `push` 和 `fetch` 使用名称传输哈希 ID,但是重要的是哈希 ID 标识的提交;和
- 对于 `git merge` 或 `git rebase`,您当前的分支很重要。在推送或提取期间,当前分支并不重要,但是如果使用 `git pull`,则会运行 `git merge` 或 `git rebase`,现在当前分支很重要。
分支名称只是指向(单个)提交的指针。
Git 全都是关于提交。从某种意义上说,Git “喜欢”我们这些凡人不需要分支名称,只需一直按哈希 ID 谈论提交即可。我可能会问您是否正在使用提交 `95ec6b1b3393eb6e26da40c565520a8db9796e9f`,您会回答“是”或“否,但我有那个”或“没有,我还没有听说过那个”。
您提到:
module1 是从 master 分支分支切出来的,feature1 是从 module1 分支分支切出来的。
但在 Git 的眼中,分支并不从另一个分支分支切出。相反,每个提交链接到以前的或“parent”提交。你画了这个:
master
----------
| module1
----------
| feature1
------------
这让我觉得你认为提交只属于一个分支或者说只在一个分支上。但 Git 并不是这样看待它们的。相反,大多数提交同时存在于许多分支上。例如,考虑我们可能会绘制成这样的图形:
o--o--o <-- br1
/
...--o--o--o <-- master
\
o--o <-- br2
每一轮中,o
代表一个提交记录。所有沿着中间行的提交记录都在 master
分支上,但是大部分这些提交记录也在其他分支 br1
和 br2
上。最后(最新和最右侧)的 master
分支上的提交记录仅存在于 master
分支上;其他的提交记录同时也在其他分支上。
这是因为,在 Git 中,分支名称如 master
只指向一个提交记录。该名称所指向的提交记录是最靠右的那个,如果我们以这种方式绘制提交历史图,则从左(早期)到右(晚期)。Git 将其称为 tip 提交记录,有时也称为该分支的 head(请注意此处为小写)。要查找您或 Git 可以从此 tip 提交记录到达的其余提交记录,Git 将通过其大而丑陋的哈希 ID 查找提交记录,例如 95ec6b1...
。再次强调,正是 哈希 ID 让 Git 找到了提交记录。而 名称 只是让 Git 找到了该哈希 ID!
提交记录本身存储有一个父提交记录的哈希 ID,因此 Git 将通过其哈希 ID 查找父提交记录,从而找到上一步的提交记录。该提交记录又有另一个父 ID,以此类推。逐步向后遍历此父哈希 ID 序列,每次一个提交记录,从后往前,这就是我们看到 git log
的方式。
如果您运行:
git checkout br1
然后进行一些工作,然后运行:
git add -a && git commit
并创建一个新提交 —— 让我们用 *
代替 o
以便能看得清楚 —— 这里展示了分支名称 br1
发生的变化:
o--o--o--* <-- br1 (HEAD)
/
...--o--o--o <-- master
\
o--o <-- br2
使用 (HEAD)
(注意:全大写)来区分我们给 git checkout
命令取的名字。新提交的 *
加入到图表中,并向后指向此前指向 br1
分支结尾的那个提交。同时,Git 把名字 br1
改为指向我们刚刚创建的新提交。这就是 Git 中分支的增长方式:我们向图表中添加新的提交,Git 更新 HEAD
所附着的名称。
当然,从 br1
和 master
的最新提交开始并向后追溯,我们最终会回到一个相同的交汇点提交,这不是巧合。但是这些提交的交汇并不源于名称。从 br1
和 master
的末端提交开始向后追溯,以任何方式开始都没有关系;重要的是这两个特定的提交,以及我们沿途找到的每个提交。
换言之,分支名称让我们在提交图表中开始工作,但是最重要的是提交图表本身。 名称只是起点!
push
的反义词是 fetch
以上所有内容都是关于在单一的 Git 存储库内工作。但是在使用 Git 时,我们通常需要与多个存储库交互。特别地,我们有自己的存储库,在其中进行自己的工作,但是我们经常需要与存储在其他机器上的另一个 Git 存储库进行通信,例如 GitHub 提供的存储库,或者雇主、朋友或同事提供的存储库。
这意味着我们要共享提交。正如我们在前面看到的,Git 的关键在于提交和提交图表,但是我们这些普通人需要名称来开始工作。这就是 git fetch
命令及其相对应的 git push
命令的作用。运行任何一个命令都会连接我们的 Git 到另一个 Git。
我们的 Git 包含了所有提交记录,其中有些可能是我们自己提交的,也有些可能是从其他地方获取到的。同样地,他们的 Git 也包含了所有的提交记录,其中有些与我们自己提交的相同,也有些可能不同。但无论如何,这些提交记录都有唯一的哈希 ID 来进行标识,在每一个 Git 中都是相同的,全球通用。如果他们没有我们的提交(因为我们刚提交了它们),那么我们新的提交将会拥有一个不同于他们所有提交记录的哈希 ID!(这看起来像是魔法,但其实只是一种基于加密数学的算法,类似于比特币背后的某些技术,不过 Git 使用的是一组更弱的哈希函数。)
这最终意味着,这两个 Git 可以通过查看这些哈希 ID 来确定对方所具有的我们没有的提交记录。这就是我们的 Git 如何将我们拥有而他们没有的提交记录推送给他们(git push),或者他们的 Git 如何将他们拥有而我们没有的提交记录获取到(git fetch)。
一旦发送了提交对象(以及其他相关的 Git 对象,以使这些提交记录完成),这两个 Git 就需要为任何新的“tip”提交设置名称。这是分支名称开始变得重要的第二个地方。
获取操作比较简单。你的 Git 向另一个 Git 发起请求。通常我们使用名字 origin 代表某个 URL,其中另一个 Git 正在监听请求,因此我们运行 git fetch origin。你的 Git 联系那个 Git,并询问它:“你的分支 tip 名称是什么?对应的哈希 ID 是多少?”他们的 Git 告诉你的 Git 关于它的分支 tip 和哈希 ID 的信息,你的 Git 要么说:“啊,我有那个哈希 ID”,要么说:“嗯,我没有那个哈希 ID,请发送给我那个提交记录,顺便问一下它的父哈希 ID,因为也许我也需要它”,等等。
最终,你的 Git 拥有了它们建议的所有提交记录和其他哈希对象。现在你的 Git 获取到了他们的分支名称,例如他们的 master 分支,并将这些名称“保存”起来。但你也有自己的分支。你的 Git 不能将这些名称保存为“你”的分支,而是对它们进行重命名。他们的分支名称,例如 master,变成了你的 Git 的“远程跟踪名称”,例如 origin/master。请注意,你的 Git 只是在他们的分支名称前加上你的简写名称 origin 和斜线。
一旦git fetch
完成,你的Git现在记住了他们 Git的分支指针位置,使用你的origin/*
远程跟踪名称。你拥有他们的提交,以及任何必要的相关内容,以便可以检出这些提交并获取与之对应的文件,但是任何新的提交只能通过这些远程跟踪名称找到。如果它们作为分支指针提交了你的旧提交,则你可能已经有其他方法找到它们。
与git fetch
相对应的是git push
,但它不完全对称。例如,要对origin
进行git push
,你的Git像之前一样呼叫他们的Git,但这次你想要发送东西给他们。这个发送操作的第一部分是交付任何你拥有的、他们没有的、他们需要的提交。你通过让你的git push
带上额外的参数来识别这些提交:
git push origin module1:module1
请注意,我在这里重复了相同的名称。左边的名称 module1:
是为了找到您要发送给他们的 特定提交哈希值 。那是您的 module1
的 最新提交。右边的名称 :module1
部分是您想让他们使用的 名称。(这些名称不必相同!但是如果可能的话,请避免在每个侧面使用不同的名称,因为这会变得棘手。)
当您运行git fetch origin
时,通常希望获取他们拥有而您没有的所有内容。这很安全,因为无论他们在master
上拥有什么最新提交,您的 Git 都将调用您的 origin/master
。无论他们在module1
上拥有什么最新提交,您的Git 都将称之为origin/module1
。您的远程跟踪名称是您自己的私有条目,全部保留给名为origin
的一个远程主机,立即将它们全部更新也是无害的甚至是一件好事。1
但是git push
方式不同。您发送一个提交哈希ID,然后要求他们将其分支名称,如master
或module1
或feature1
,设置为该哈希ID。如果他们还没有那个提交哈希ID,他们会让你的 Git 发送提交对象(和需要的所有父提交和其他对象,都由哈希ID标识),然后他们自行评估是否允许您设置他们的分支名称。有时他们会这样做,您的推送就会成功;有时他们会找到一条规则说“不允许这样做”,您的推送失败。
请注意,他们的规则由他们制定!您发送请求(“请将您的module1
设置为a9fc312...
”);他们可以查看当前的module1
分支哈希ID以及您发送的提交(如果是新的),并选择是否接受请求。您可以使用--force
将其发送为命令,但即使这样做,他们仍然可以选择是否遵守该命令。您所能做的就是提出请求(或强制性命令),然后查看他们是否接受。但是,大多数Git通常使用的标准规则是:只要请求添加新提交,就将其允许。
当你向
br1
添加提交时会发生什么,请看上面的内容。所有现有的提交都是从原始尖端提交可达的,它们仍然可以从新的尖端提交可达。新的提交“扩展了该分支”,它没有更改任何现有的提交,因为任何新的提交都不能更改任何现有的提交,但是新提交的父提交是旧尖端提交。如果我们从新的尖端开始向后工作,就像Git一样,我们会到达旧的尖端。
这些规则在
git push
中也适用:如果您发送的请求将保持所有现有提交可从新分支尖端到达,则另一个Git可能会允许该请求。
请注意,我们一直在使用
git push origin module1:module1
,但您建议我们运行:
git push origin module1 //Being in feature1 branch
无论我们在哪个分支中(或者像`git status`所说的那样,在分支`feature1`上),都没有关系。重要的是`git push`命令中的两个`module1:module1`部分。这告诉我们的Git:使用我们的名字`module1`来找到要推送的提交,并询问他们的Git设置他们的`module1`名称。
我们只提供了一个部分,而不是两个部分。对于`git push`,如果你只提供一个部分,Git会假设你的意思是“将那个名字用两次”。所以,`git push origin module1`实际上意味着`git push origin module1:module1`。
`git pull`是`git fetch`后跟第二个Git命令
然后您问了这个问题:
git pull origin module1 //Being in feature1 branch
git pull
的作用很容易描述:
它将使用传递给它的大多数参数运行 git fetch
:
git fetch origin module1
如果这样做有效,它会运行第二个Git命令,你需要预先选择。默认要运行的命令是git merge
。第二个命令的确切参数取决于git fetch
期间收到了什么,但在查看结果之前,您仍然需要选择git merge
与git rebase
。
我们之前提到,通常只运行git fetch origin
,它会带来所有origin
拥有的名称,然后您的Git进行重命名。如果您运行:git fetch origin module1
这只是简单地限制了fetch的范围:它找到他们的module1
,根据需要通过ID将提交复制过来,然后设置你的origin/module1
。2 它忽略所有其他名称。这里的module1
很像于git push
中的module1
,除非您不使用两个名称——如果您没有写module1:module1
——您的Git将只更新您的远程跟踪名称。(在这里很少需要甚至从不需要使用由冒号分隔的两个名称。它确实可以工作并且可用于某些目的,但您需要了解更多细节。)
在git fetch
期间,您检出哪个分支并不重要。但第二个Git命令是git merge
或git rebase
,对于这两个命令,您检出的分支很重要。
git pull
运行的第二个命令,在运行时不会更改当前的分支。假设第二个命令是git merge
,参数是:
- 从远程Git获取的分支尖端哈希ID3
- 消息"merge branch 'name' of url"
这很像运行git merge origin/name
,尽管消息略有不同。
git merge
对此的处理本身有点复杂,因为git merge
有时什么也不做,有时会快进而不是合并,有时会真正合并,并且有时会出现合并冲突,使合并在中途停止并寻求您的帮助。所有这些内容我将留给其他答案。
让我们在这里总结一下:
您的git push
和git fetch
操作始终传输整个提交。它们从一个Git存储库复制提交到另一个存储库,从而通常涉及在接收方Git中设置一些名称,以便记住任何调整的尖端提交。在大多数情况下,这些都不关心您当前的分支(尽管可以通过默认情况下推送当前分支来告诉git push
:请参见push.default
)。
您的git pull
只是运行两个单独的Git命令。如果您对这两个命令都不是非常熟悉,我建议您分别运行每个命令。
你的git merge
命令是最复杂的一个,应该单独提出来作为一个问题。如果你认为自己没运行过git merge
命令,请参考上面的git pull
。
1获取全部的缺点是:如果有很多"全部"且你的网络连接较慢,那么可能需要花费很长时间。另一方面,如果你今天获取了所有内容,那么明天几乎就没有什么可获取的了,所以下一个git fetch
将会更快。如果你只获取了一点内容,那么可能会导致明天的git fetch
变慢。
2这假设你的Git版本至少是1.8.4。在1.8.4之前的Git中,git fetch origin module1
会带来它们的哈希ID,但然后抛弃名称,未更新你自己的origin/module1
。
3如果你运行git pull origin name1 name2
,Git会向git merge
传递多个哈希ID,从而生成Git称为章鱼合并的内容。这几乎永远不是你想要的,所以要避免这种情况,就不要使用git pull
命令!