首页 > 解决方案 > How are different branches stored locally from git on my disk?

问题描述

I have only one version repository sitting on my local HDD but multiple branches on Github. Shouldn't there be copies of code per branch? what version of code do I have sitting on my local disk?

标签: gitgithubversion-control

解决方案


每个分支不应该有代码副本吗?

不,这不是 Git 的分支名称的工作方式。

从某种意义上说,Git 存储的根本不是分支。Git 存储提交。提交几乎是 Git 中的一切。分支机构的出现要晚得多,它们的作用相当小。当然,它们对人类很重要,因为任何提交的实际名称都是一个对人类无用的大而丑陋的哈希 ID。分支名称让我们使用名称而不是哈希 ID。但 Git 最关心的是提交,以及它们的哈希 ID。

我的本地硬盘上只有一个版本存储库,但 Github 上有多个分支。

通常,本地驱动器上的存储库是 GitHub 上存储库的克隆。或者,我们同样可以说 GitHub 上的存储库是本地驱动器上存储库的克隆。两个存储库都不比另一个“更好”,它们只是拥有不同的计算机来保存它们,并且提交集可能略有不同,并且其中的分支名称更可能不同(但由于 Git 更关心提交,那并不对 Git 来说非常重要)。

要了解这是如何工作的,请从提交开始。每个提交——由一些大而丑陋的哈希 ID 命名,例如8a0ba68f6dab2c8b1f297a0d46b710bb9af3237a——存储一些文件集的完整快照。除了那个快照,每个提交都有一些元数据,一些关于提交的数据。例如,如果您自己进行了提交,则该提交将存储您的姓名和电子邮件地址,以及您进行提交的时间戳。而且,每个提交通常存储其先前或提交的哈希 ID。8a0ba68f6dab2c8b1f297a0d46b710bb9af3237a(例如,上面的父级是15cc2da0b5aaf5350f180951450e0a5318f7d34d。)

Git 使用这些链接来查找提交。让我们考虑一下您最近创建的一个小型存储库,其中您只进行了 3 次提交。每个提交都有一些丑陋的哈希 ID,但让我们使用单个大写字母。因此,您所做的第一个提交是 commit A。因为它第一次提交,所以它没有父级。

然后,通过提交A,您提交了B。它有A它的父母。同样,您曾经Cmake B, so CstoresB的哈希 ID。

每当我们有某个提交的哈希 ID 时,我们就说我们可以指向那个提交。所以C指向BB指向A。如果我们画这个,我们得到:

A <-B <-C

现在,使用简单的单个大写字母和这张图,很明显我们需要从头开始并向C后工作。这就是 Git 所做的,但Git 查找 commit的方式C是使用分支名称。由于我们刚刚启动了这个存储库,我们可能仍在使用master,所以让我们把它画在:

A <-B <-C   <--master

该名称master包含 commit 的哈希 ID C,以便 Git 可以找到C. 从那里,Git 可以向后工作,B然后A.

如果我们想添加一个的提交,我们运行git checkout master,它会得到一个我们可以处理的副本C,并记住我们现在正在处理master的是 commit C。我们git add根据需要进行工作、归档并运行git commit. Git 进行新的提交D(从index中的任何内容,我不会在这里定义),并设置D为指向C

A--B--C   <--master
       \
        D

现在棘手的部分发生了:由于我们刚刚创建D,并且我们正在运行master,Git 现在使用新提交的任何真实哈希 ID 进行更新 。所以现在指向,而不是:masterDmasterDC

A--B--C
       \
        D   <--master

我们可以理顺图中的扭结(我也想稍微放松一下箭头):

A--B--C--D   <-- master

现在假设我们决定添加一个新分支。我们运行,例如,git checkout -b develop。这样做只是添加一个新名称,但保留所有相同的提交。新名称 ,develop指向我们选择的提交,默认为我们现在使用的那个,即 commit D

A--B--C--D   <-- master, develop

现在我们需要一种方法来绘制我们正在使用的分支名称。为此,我们会将单词HEAD(全部大写)附加到两个分支之一。由于我们确实git checkout附加HEAD到 new develop,让我们绘制:

A--B--C--D   <-- master, develop (HEAD)

现在是时候进行另一个新的提交了。不做任何其他事情,我们修改一些文件,用于git add将更新的文件复制到索引中,然后运行git commit以进行新的提交,我们将调用E它(但像往常一样,它会获得一些难以理解的哈希 ID)。当 Git 更新分支名称时,它更新的名称是HEAD附加到它的名称,所以图表现在看起来像这样:

A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)

此时,假设我克隆了您的存储库(直接从您的机器,或从您发送到 GitHub 的精确副本,具有相同的提交和相同的分支名称)。我的 Git 首先会这样做:

A--B--C--D   <-- origin/master
          \
           E   <-- origin/develop

在这里,我没有任何分支。我有所有相同的提交,但记住它们的目的不是分支名称,而是远程跟踪名称。而不是master,我必须origin/master记住 commitD的哈希 ID。而不是develop,我必须origin/develop记住E的哈希 ID。

作为我克隆的最后一步,我自己的 Git 尝试将git checkout master. 而不是因为我没有失败而失败master这实际上创建了my ,master使用我origin/master的 Git 从你的 Git 复制的master. 所以现在我有:

A--B--C--D   <-- origin/master, master (HEAD)
          \
           E   <-- origin/develop

如果我现在运行git checkout develop,我的 Git 将查看我的存储库并且不会找到 a develop,但它找到一个origin/develop. 然后我的 Git 将创建一个新名称,develop指向 commit E,并附HEAD加到该名称而不是 to master

A--B--C--D   <-- origin/master, master
          \
           E   <-- origin/develop, develop (HEAD)

请注意,没有任何提交被复制。Git 只是添加了一个新名称,指向一些现有的提交。现有的提交已经存在,在我的本地存储库中。

fetch您使用和连接两个 Git 存储库push

这是您从 GitHub 存储库获取更新的方式。如果他们有一些您没有的新提交,您可以git fetch在您的 Git 存储库中运行。您的 Git 使用origin您的 Git 在原始git clone操作期间创建的名称来查找 GitHub 的 URL。然后你的 Git 调用 GitHub,你的 Git 和他们的 Git 进行了一些对话。

使用git fetch,您的 Git 会向他们的 Git 询问他们的分支名称以及每个名称的最终提交。他们的 Git 可能会说:我的master观点是一些带有大而丑陋的哈希的提交——让我们称之为这个H,而不是试图猜测实际的哈希 ID。你的 Git 看着你的存储库并对自己说:我没有H,我最好要求它。 你的 Git 询问他们的 Git H;他们说H'父母是G,谁的父母是F,谁的父母是D。你们俩都有D,所以这部分对话结束了。他们的 Git 也可能会说:my developpoints to commit E 你的 Git 已经有了E,所以这部分对话也完成了。没有其他名字需要担心,所以现在你的 Git 让他们的 Git 发送提交F,GH,你的 Git 将它们保存在你的存储库中:

           F--G--H   <-- origin/master
          /
A--B--C--D   <-- master
          \
           E   <-- origin/develop, develop (HEAD)

请注意,除了将提交添加到您自己的存储库之外,您的 Git 所做的唯一另一件事就是更新您的所有origin/*名称,以便它们与其他 Git 的名称匹配。也就是说,您origin/master已经从 shared commitD转移到 now-shared commit H

Fetch 总是安全的,但git push不一样

运行总是安全的git fetch:这会将您的 Git 连接到其他 Git,从它们那里获取任何提交,并更新您的远程跟踪名称。因为这些名字只是记住他们的工作,而不是你的工作,所以很安全。如果你做了新的提交,你的新提交仍然在你的分支上,而不是他们的分支,除了你们共同的任何提交。

当你使用 时git push,你的 Git 会调用他们的 Git,这两个 Git 会进行非常相似的对话。如果你有新的提交,你的 Git 会为他们的 Git 提供一些新的提交。但是,然后,您的 Git 不会更新您的远程跟踪名称,而是您的 Git 向他们的 Git 提供了一个礼貌的请求:如果可以,请更新 yourmaster以匹配 my master或者如果可以,请更新 yourdevelop以匹配 mydevelop ——或者也许甚至两者。(您可以一次推送任意数量的分支名称。)

假设在之前的 之后git fetch,你有这个:

           F--G--H   <-- origin/master
          /
A--B--C--D   <-- master
          \
           E   <-- origin/develop, develop (HEAD)

你一直在develop做一些新的提交,我们称之为Iand J,所以现在你有了这个:

           F--G--H   <-- origin/master
          /
A--B--C--D   <-- master
          \
           E   <-- origin/develop
            \
             I--J   <-- develop (HEAD)

但是,你不知道的是,其他他们的 develop. 该提交具有您在任何地方都没有的哈希 ID。K让我们在他们的Git 中调用它,这样他们就有了:

           F--G--H   <-- master
          /
A--B--C--D
          \
           E--K   <-- develop

你发送给他们Iand J,所以现在他们有这个:

           F--G--H   <-- master
          /
A--B--C--D
          \
           E--K   <-- develop
            \
             I--J   <-- (polite request to make develop go here)

如果他们接受你的请求,并移动他们的develop,他们的 commit 会发生什么K?他们最终得到了这个:

           F--G--H   <-- master
          /
A--B--C--D
          \
           E--K   ???
            \
             I--J   <-- develop

如上所述,Git 处理提交和分支名称的方式是它使用分支名称来查找最后一个提交,然后向后工作。从 向后工作J,他们的 Git 会转到J. 从J他们会回到I, 然后E, 然后D, C, B, 和A。这意味着提交K丢失了!

结果,他们的 Git 会拒绝你的礼貌请求:,如果我移动了我的develop,我会丢失一些提交。 (您的 Git 将此报告为“拒绝”和“不是快进”。)

在这种情况下,您需要做的是git fetch获取他们的新提交K,然后在您自己的存储库中做一些事情来适应它。不过,这是针对另一组问题的(所有这些问题都已在 StackOverflow 上得到解答)。


推荐阅读