当在一个分支中时,执行Git pull或push到另一个分支会发生什么?

5

我熟悉GIT,并将其用于我的项目版本控制。

我有一些问题想要了解。我尝试过搜索,但没有找到好的答案。

所以,我有主分支(master)模块1(module1)特性1(feature1)三个分支。

master
 ---------- 
         | module1
         ----------
                  | feature1
                  ------------

module1 是从 master 分支分出来的,feature1 又是从 module1 分支分出来的。

问题 1:如果我在 feature1 分支上进行一些更改并提交和推送到 module1 分支会怎样?

git add .
git commit -m "Changes of feature1"
git push origin module1 //Being in feature1 branch

在此,feature1到module1分支的代码会发生什么,以及module1分支如何接收它。
我的理解是:根据我的理解,feature1的更改将被推送到module1分支。后来我意识到应该将其推送到feature1分支,然后我会将相同的更改推送到feature1分支,然后切换到module1分支并还原我最近推送的代码。
第二个问题是:如果我在feature1分支中,并在此分支中通过以下方式拉取了module1的代码,会发生什么?
git pull origin module1 //Being in feature1 branch

我的理解: 模块1代码的更改将合并到我的feature1分支,与以下命令相同

git checkout moduel1
git pull origin module1
git checkout feature1
git merge module1

如果存在冲突,将会显示出来。我需要解决这些冲突。

请问是否有人能帮助我确认我的理解是否正确。如果不正确,请帮助我更好地理解这个概念。提前感谢您的帮助。

3个回答

16
你对分支名称和使用`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 分支上,但是大部分这些提交记录也在其他分支 br1br2 上。最后(最新和最右侧)的 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 所附着的名称。

当然,从 br1master 的最新提交开始并向后追溯,我们最终会回到一个相同的交汇点提交,这不是巧合。但是这些提交的交汇并不源于名称。从 br1master 的末端提交开始向后追溯,以任何方式开始都没有关系;重要的是这两个特定的提交,以及我们沿途找到的每个提交。

换言之,分支名称让我们在提交图表中开始工作,但是最重要的是提交图表本身。 名称只是起点!

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,然后要求他们将其分支名称,如mastermodule1feature1,设置为该哈希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 的作用很容易描述:

  1. 它将使用传递给它的大多数参数运行 git fetch

  2. git fetch origin module1
    
    如果这样做有效,它会运行第二个Git命令,你需要预先选择。默认要运行的命令是git merge。第二个命令的确切参数取决于git fetch期间收到了什么,但在查看结果之前,您仍然需要选择git mergegit rebase
    我们之前提到,通常只运行git fetch origin,它会带来所有origin拥有的名称,然后您的Git进行重命名。如果您运行:
    git fetch origin module1
    

    这只是简单地限制了fetch的范围:它找到他们的module1,根据需要通过ID将提交复制过来,然后设置你的origin/module12 它忽略所有其他名称。这里的module1很像于git push中的module1,除非您不使用两个名称——如果您没有写module1:module1 ——您的Git将只更新您的远程跟踪名称。(在这里很少需要甚至从不需要使用由冒号分隔的两个名称。它确实可以工作并且可用于某些目的,但您需要了解更多细节。)

    git fetch期间,您检出哪个分支并不重要。但第二个Git命令是git mergegit rebase,对于这两个命令,您检出的分支很重要。

    git pull运行的第二个命令,在运行时不会更改当前的分支。假设第二个命令是git merge,参数是:

    • 从远程Git获取的分支尖端哈希ID3
    • 消息"merge branch 'name' of url"

    这很像运行git merge origin/name,尽管消息略有不同。

    git merge对此的处理本身有点复杂,因为git merge有时什么也不做,有时会快进而不是合并,有时会真正合并,并且有时会出现合并冲突,使合并在中途停止并寻求您的帮助。所有这些内容我将留给其他答案。

    让我们在这里总结一下:

    • 您的git pushgit 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命令!


如果我可以投多次赞,我一定会这么做。 - Chris
被低估的答案是被低估的。 - Eduardo

1
你的理解大体正确。我认为跟随一些关于Git分支的教程,或者对简单存储库进行一些实验,将有助于巩固你对所描述情况的理解。
针对你的具体问题,如果我们假设你已经分支两次,并且没有进行任何提交:
Master  |  A -> B 
--------        ^ 
Module  |       | (B)  
--------        ^ 
Feature |       | (B) 

Query 1:如果您正在Feature分支上工作,提交了一些更改并将它们推送到Module上,该怎么办?
因此,首先在Feature上进行了一些新的提交,假设有2个附加提交:
Master  |  A -> B 
--------        ^ 
Module  |       | (B) 
--------         \
Feature |         C -> D

假设在Module上没有其他提交,你将更改推送到Module,则可以将更改图形表示如下:

Master  |  A -> B 
--------         \ 
Module  |         C -> D
--------               ^
Feature |              | (D)

你从Feature提交的更改仅被应用在Module分支上(假设快速合并)。更准确地说,Module分支指针移动到提交“D”,因此它实质上指向与Feature相同的提交系列。注意:我有意不提及强制合并提交、变基或其他潜在的复杂情况。
然后你决定不想在Module分支上使用这些更改,潜在的问题在于你使用了“还原”一词。我认为你所描述的是使用git reset <commit hash for B>Module分支指针移回到B,这本质上重置了你推送更改到Module之前的状态。
Master  |  A -> B 
--------        ^ 
Module  |       | (B) 
--------         \
Feature |         C -> D

请注意,如果您实际上使用 git revert <要还原的提交哈希>撤销提交,那么您将在Module分支中创建新的提交,这将撤消引入的更改。我相信这不是您所描述的情况。
查询2:您的理解基本正确。git pullgit fetch 后跟着 git merge,因此可以视为等效于:
git fetch origin module1
git merge origin/module1

正如您所说,任何在Module中的更改,如果不在当前分支中,则会被合并(请注意,故意假设没有变基)。 如果有合并冲突,git无法解决,您需要手动解决冲突。

一些有用的链接:


-1

根据不同情况,两个查询的答案应该是不同的:

情况1:feature1分支是从最新的module1分支中分支出来的(在创建feature1分支后,在module1分支上没有进行任何提交)。

提交历史如下:

---A---B---…    master
        \
         C---D---E    module1
                  \
                   F---G---H   feature1
  • 如果您从本地的feature1分支将更改推送到远程的module1分支,则可以成功,远程存储库中的module1分支将指向提交H
  • 如果您从远程的module1拉取更改并应用到本地的feature1分支,它不会拉取任何提交到本地的feature1分支,并且它会显示已经是最新的。

情况2:在从feature1分支创建后,module1分支上有新的提交。

提交历史记录如下:

---A---B---…    master
        \
         C---D---E---I---J    module1
                  \
                   F---G---H   feature1
  • 如果你从本地的feature1分支推送更改到远程的module1分支,Git会停止推送并提示你先进行拉取操作,因为上图中的提交IJfeature1分支中不存在。
  • 如果你从远程的module1分支拉取更改到本地的feature1分支,它将把来自origin/module1分支的更改合并到feature1分支中(正如你所想的那样)。

即使在不同的分支上进行拉取/推送可能会作为快进或合并方式工作,但最好在其自己的本地分支上执行拉取/推送操作。这可以使不同分支上的工作更加清晰。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接