-1

假设我克隆了一个远程存储库,到目前为止它有 1 个 commit => A。然后,我对我的本地分支进行了两次提交,所以它变成了 => A - B - C。但是,我的同事同时向他们的本地分支提交了另外两个提交,所以他们的提交历史变成了 => A - D - E。然后他们将其推送到远程存储库。

然后我意识到我想推送我的更改,但git push告诉我远程存储库在我前面。所以,我愿意git pull

我的问题是,现在跟踪远程跟踪分支的本地分支是什么样的?我知道会有合并冲突,但我的实际问题是:提交历史会是什么样子?

更具体地说,假设我修复了冲突并现在提交了它们,我的提交历史会看起来像这样A - D - E - F还是A - B - C - D - E - F?git中的提交历史是非线性的吗?

4

3 回答 3

1

是的,如果是正常拉动,将与 2 个父分支合并......所以你将有两个平行的分支,比如说。用 来看看结果git log --all --graph。作为旁注:冲突不是强制性的。他们出现的原因有很多,但在合并时并不是给定的。您可能会经常使用无冲突的合并。

于 2021-07-28T14:33:01.343 回答
1

最短的答案(不是 100% 准确,但非常接近)是git pull 根本不管理历史。对你git pull有用的是运行两个 Git 命令,作为初学者,我建议你自己分别运行:

  • 首先,git pull执行一个git fetch. 这个命令非常简单直接(但有一些曲折)。它从其他存储库获取新的提交:您的 Git 调用其他 Git,您的 Git 和他们的 Git 交换提交哈希 ID,并且从这里,您的 Git 发现您需要从它们获取哪些提交(和相关文件),以便您将通过 Internet 获得他们所有的提交,并使用相当少的数据量。

  • 完成后,git pull运行第二个 Git 命令。这是最复杂的地方。(这些第二个命令往往有很多选项、模式和功能,所以它几乎就像运行十几个命令中的一个。)

第二个 Git 命令的选择是你的,但是当你使用 时git pull,你不得不在你有机会看到会做什么之前做出git fetch选择。我认为这是不好的(大写 B 不好,但不是粗体或斜体不好,所以只有中度不好)。一旦你经常使用 Git,并且知道 fetch 是如何工作的,或许更重要的是,你已经发现了某些同事或同事或朋友如何使用 Git——这些都会影响git fetch将要做什么——就可以安全地决定如何集成在获取提交之前获取提交。但在早期,我认为这个要求有点过分。1


1总是可以撤消第二个命令所做的事情,但您需要了解有关第二个命令的所有信息。作为初学者,您甚至可能没有意识到这里有两个不同的命令。您肯定不会知道足够多的知识来撤消每个命令的每个模式的每个效果。


之后你有正确的设置git fetch

假设我克隆了一个远程存储库,到目前为止它有 1 个 commit => A。然后,我对我的本地分支进行了两次提交,所以它变成了 => A - B - C。但是,我的同事同时向他们的本地分支提交了另外两个提交,所以他们的提交历史变成了 => A - D - E。然后他们将 [this] 推送到 [a shared remote] 存储库。

当他们击败您并且他们git push对共享(第三)存储库“获胜”时,该共享第三存储库的提交现在具有以下A-D-E形式:

A--D--E   <-- main

(这里的分支名称并不是那么重要,但我正在使用它,因为 GitHub 现在使用它作为它们的默认名称,并且您在标签main中提到了

git fetch一步让你得到的是提交DE. 您已经有 commit A,并且在提交后不能更改任何提交。2 所以你只需要D-E,它会像这样在你的存储库中结束:

  B--C   <-- main
 /
A
 \
  D--E   <-- origin/main

该名称origin/main是您的 Git 的远程跟踪名称,您的 Git 从其 Git 的分支名称创建该名称main。您的 Git 获取每个 Git 的分支名称并更改它们,以创建这些远程跟踪名称。由于远程跟踪名称不是分支名称,因此对它们所做的任何更改git fetch(以处理其他 Git 存储库中发生的任何事情)都不会影响您的任何分支。因此,运行总是安全的git fetch3

A我在自己的行上画了提交,以强调它只是一个提交,由两个开发线共享。而且——需要考虑的一点——如果一个分支是一个开发线,那么它不是origin/main一个分支吗?这是对“分支”的模糊定义,4但它很快就会变得有用。


2请注意git commit --amend,例如,实际上并不会更改提交。相反,它会进行的提交,并让您使用它而不是您正在使用的其他提交。您现在有两个几乎相同的提交,其中一个只是被推到一边并被忽略了。

3可以设置git fetch,或者给它参数,让它做“不安全”的事情,但这很难。通常的简单方法是进行镜像克隆,但镜像克隆也是自动--bare进行的,裸克隆不会让您在其中做任何工作。(镜像克隆只适用于特殊情况,不适用于日常工作。)

4 Git 对分支的定义刻意弱化和模糊,小心说出分支名称会有所帮助。分支名称定义明确,不会出现这种哲学上的歧义。远程跟踪名称分支名称明显不同,尽管这两种名称都让 Git 找到提交,并且提交本身形成了我们(人类)喜欢认为的“分支”。所以从这个意义上说,origin/main是一个找到分支的名称。它只是不是一个分支名称:在内部,它拼写为refs/remotes/origin/main,分支名称必须以 . 开头refs/heads/分支名称main拼写_refs/heads/main内部。另请参阅“分支”到底是什么意思?


第二个命令:你的选择git mergegit rebase

运行的第二个命令git pull是大多数实际操作发生的地方。这是要么git merge,要么git rebase5 这些处理您与git fetch. 每个人都使用不同的方法。

从根本上说,合并比变基更简单。这是因为 rebase 是通过复制提交来工作的,就像通过运行一样git cherry-pick——一些形式的git rebase字面使用git cherry-pick,而另一些形式使用近似值——并且每个樱桃选择本身就是一种合并。这意味着,例如,当您对三个提交进行 rebase 时,您将执行三个合并。rebase 执行的复制之后是另一个内部 Git 操作,而许多形式的操作git merge都是一步完成的。


5从技术上讲,git pull可以git checkout在一种特殊情况下运行,但这种情况不适用于此处。


合并

从根本上说,合并是关于合并工作。

请注意,当我们遇到像我们上面描述的那样的情况时,我们必须合并工作,其中一些共同的起点(提交A)之后是发散的工作。但是,在某些情况下“组合工作”是微不足道的:

A   <-- theirs
 \
  B--C   <-- ours

在这里,“他们”——不管他们是谁——实际上并没有做任何工作,所以要将你的工作与他们的工作“结合”,你可以让 Git 切换到你最近的提交:

A--B--C   <-- (combined successfully)

Git 将这种“合并”称为快进操作,而何时git merge执行,Git 将其称为快进合并。一般来说,如果git merge 可以做一个快进合并,它做一个。如果没有,它将进行全面合并。

完全合并找到一个合并基础——两个分支上的共享提交,使用我前面提到的故意松散的分支定义,并将该特定提交中的快照与两个分支提示提交中的快照进行比较。这允许 Git 弄清楚“我们改变了什么”以及“他们改变了什么”:

  B--C   <-- main
 /
A
 \
  D--E   <-- origin/main

A从to的差异C显示了我们在两次提交中所做的更改。A从to的差异E显示了他们两次提交中所做的更改。

然后,Git 尝试将两组更改组合并应用到 commit 中的快照A。如果 Git 认为这一切顺利,Git 将继续从结果中创建一个新的快照——一个新的提交。通过获取我们的更改并添加他们的更改(或者,等效地,获取他们的更改并添加我们的更改),Git 的合并提交将具有作为其快照的?正确?组合。这里的问号是因为 Git 只是使用简单的逐行规则。结果在其他意义上可能不正确:它只是按照 Git 的规则正确。

无论如何,Git 现在将进行的新合并提交链接回我们当前的提交和他们C 提交E

  B--C
 /    \
A      F   <-- main
 \    /
  D--E   <-- origin/main

我们的分支名称,main现在选择新的合并提交F。请注意,它F有一个快照,就像任何普通的提交一样,还有一个日志消息和作者等等,就像任何普通的提交一样。唯一特别的是F,它不是指向一个先前的提交,而是指向两个。

然而,这会产生巨大的后果,因为 Git查找提交的方式是从某个名称开始——通常是分支名称,尽管任何名称都可以——并使用它来定位最后一次提交,然后按照所有向后链接到所有以前的提交。因此,从 开始F,Git 会倒退到“同时”CE“同时”。6


6由于这不太可能,Git 必须使用某种近似值。Git 的某些部分使用广度优先搜索算法,而其他部分则使用各种技巧。


变基

从根本上说,变基是关于接受一些“还可以,但不够好”的提交并将它们复制到(据说)更好的新的和改进的提交中,然后放弃原件以支持新的和-改进的副本

这样做有几个问题:

  • Git“喜欢”添加新提交。它“不喜欢”丢弃旧的提交。Rebase 迫使 Git 抛弃旧的,转而支持新的和改进的,就目前而言这很好,但是......

  • 我们将提交从一个 Git 存储库发送到另一个。一旦它们被复制——一旦马离开谷仓并被克隆——摧毁它们中的一些是没有好处的。如果我们有新的和改进的替代品,我们必须让每一个拥有原始副本的Git拿起并切换到新的和改进的替代品。这意味着我们需要强制其他 Git 放弃一些现有的提交。

一条始终有效的简单规则是:仅替换您从未放弃的提交。 这是有效的,因为如果您只有一个副本,那么您的新的和改进的替代品不需要让任何其他Git 来丢弃旧的。没有涉及其他 Git 存储库!但这太简单了,至少对于许多 GitHub 工作流程来说是这样。

一个更复杂的处理方法是:只有替换您和这些存储库的所有其他用户事先同意的提交才能被替换。 其他用户(至少如果他们注意的话)会注意到替换并拿起它们。

在不深入了解所有细节的情况下,要做git rebase的是:

  • 列出要复制的提交(哈希 ID);
  • 使用 Git 的detached HEAD模式来避免临时分支的需要;
  • 检查副本要执行的目标提交;
  • git cherry-pick使用或等效的方法一一复制要复制的提交;最后
  • 移动分支名称以指向最后复制的提交。

在这种情况下,您可以将您的两个现有提交变基(复制)到两个新的和改进的提交:

  B--C   <-- main
 /
A      B'-C'  <-- HEAD
 \    /
  D--E   <-- origin/main

其中B'和是和C'的副本。通过更改快照中的快照来构建快照;要进行的更改是通过比较看到的。中的快照类似,但通过将更改从到 进行。BCB'EABC'BC

完成所有副本后,Git 会main从旧提交上剥离旧标签C并将其粘贴到新C'提交上:

  B--C   [abandoned]
 /
A      B'-C'  <-- main (HEAD)
 \    /
  D--E   <-- origin/main

原始BC提交仍然存在一段时间,但没有简单的方法找到它们,你就再也看不到它们了。如果您没有仔细记下原始Band的真实哈希 ID C,您会认为它们的新的和改进的替代品以某种方式神奇地改变 BC到位。但他们没有:他们是全新的,旧的提交仍然存在。旧的提交根本没有被使用。一段时间后——默认情况下至少 30 天——Git 会认为它们是垃圾,并最终用它们“垃圾收集”它们git gc(Git 会自动为你运行,通过git gc --auto从各种 Git 命令中分离出来,而无需您做任何事情)。

如果一切顺利,rebase 提交会“保留”您工作的本质,让您看起来好像是在看到您的同事将要做什么之后才开始工作。但是,复制的提交中的日期和时间戳更复杂:

  • 作者日期是您最初编写提交的时间,保留。
  • 提交者日期是您上次使用 rebase 复制它们的时间。

您可以重复 rebase 提交,并且作者时间戳保留在每个副本中。例如,要查看两个时间戳,请使用git log --pretty=fuller

于 2021-07-29T03:50:52.657 回答
0

提交历史不必是线性的。假设您的朋友对某个文件进行了更改并将其推送。所以遥远的历史看起来像 A - D - E。如果您进行了一些其他更改,例如您的提交历史记录是A - B - C,那么如果存在冲突并且如果您修复了这些冲突并将您的提交推送到远程,那么远程历史记录将如下所示:

  /--D---E-\
 A          P
  \--B---C-/

P是您解决冲突的提交。(解决冲突基本上是使用已解决的更改进行新的提交。)

于 2021-07-28T18:20:57.367 回答