git 如何拉取管理提交历史?

How does git pull manage commit history?

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

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

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

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

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

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

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

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

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

  • 首先,git pull执行git fetch。此命令非常简单明了(但有一些曲折)。它从其他一些存储库获得新的提交:你的 Git 调用其他一些 Git,你的 Git 和他们的 Git 交换提交哈希 ID,从这里,你的 Git 发现您需要从他们那里获得哪些提交(和相关文件),这样您就可以获得他们的所有提交以及通过 Internet 带来的 reasonably-minimal 数据量。

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

第二个 Git 命令的 choice 是你的,但是当你使用 git pull 时,你被迫 make 在你有机会看到 git fetch 会做什么之前做出选择。我认为这是不好的(大写字母 B 不好,但不是粗体或斜体不好,所以只是中等差)。一旦你经常使用 Git,并且了解 fetch 的工作原理,也许更重要的是,发现某些同事或 co-workers 或朋友如何使用 Git——这些都会影响 git fetch 会做——在获取之前决定如何集成获取的提交是安全的。但是一开始,我觉得这个要求有点过分了。1


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


您在 git fetch

之后设置正确

Let's say I clone a remote repository, and so far it has 1 commit => A. Then, I make two commits to my local branch, and so it becomes => A - B - C. However, my coworker meantime made other two commits to their local branch, so their commit history becomes => A - D - E. And then they push [this] to [a shared remote] repository.

当他们先发制人并且他们 git push 共享(第三)存储库“获胜”时,共享第三存储库 中的提交现在拥有A-D-E表格:

A--D--E   <-- main

(这里的分支名称并不是那么重要,但我使用 main 因为 GitHub 现在使用它作为默认值,你在标签。)

git fetch 步骤得到的是提交 DE。您已经提交了 A,并且提交后无法更改任何提交。2 所以您只需要 D-E,它会像这样在您的存储库中结束:

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

名字 origin/main 是你的 Git 的 remote-tracking 名字,你的 Git 从他们的 Git 的 分支机构 名称 main。您的 Git 获取了他们每个 Git 的分支名称并更改它们,以创建这些 remote-tracking 名称。由于 remote-tracking 名称不是 branch 名称,因此 git fetch 对它们所做的任何更改——以处理其他 Git 存储库中发生的任何事情——不会影响任何你的分支。因此 运行 git fetch.3

总是安全的

我将提交 A 单独画在一行中以强调它只是一个提交,由两个 lines-of-development 共享。并且 - 需要考虑的事情 - 如果 分支 开发线 ,那么 origin/main 不是 branch,有点像?这是对“分支”的模糊定义,4但后来证明它很有用。


2请注意 git commit --amend,例如,实际上并没有 更改 提交。相反,它会进行 new 提交,并让您使用它来代替您正在使用的其他提交。您现在有两个 almost-identical 提交,其中一个只是被推到一边并被忽略。

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

4Git对分支的定义是故意弱和模糊,小心地说 分支名称 会很有帮助。分支名称是 well-defined 并且不会遭受这种哲学上的歧义。 remote-tracking 名称 明显不同于 分支名称,尽管这两种名称都可以让 Git 找到提交,并且承诺本身形成了我们(人类)喜欢认为的“分支”。所以从这个意义上说,origin/main 是一个找到分支的名字。它只是不是 branch 名称:在内部,它拼写为 refs/remotes/origin/main,其中分支名称必须是refs/heads/ 的艺术。 分支名称main在内部拼写为refs/heads/main。另见 What exactly do we mean by "branch"?


第二个命令:您选择git mergegit rebase

second 命令 git pull 运行s 是大多数实际操作发生的地方。这要么是 git merge,要么是 git rebase5 这些处理你用 git fetch 设置的分歧。每个人使用不同的方法。

从根本上说,合并比变基更简单。这是因为 rebase 通过 copying 提交来工作,就像通过 运行ning git cherry-pick 一样——某些形式的 git rebase 字面上使用 git cherry-pick 和其他人使用近似值——每个 cherry-pick 本身就是一种合并。这意味着,当您变基三个提交时,例如,您将执行三个合并。 rebase 执行的复制之后是另一个内部 Git 操作,而 git merge 的许多形式是 one-step-and-done.


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


正在合并

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

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

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

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

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

Git 将这种“组合”称为 fast-forward 操作 ,而当 git merge 这样做时,Git称其为 fast-forward 合并 。一般来说,如果 git merge 可以 做一个 fast-forward 合并,它 做一个。如果没有,它将进行 full-blown 合并。

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

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

AC 的差异显示了 我们 在我们的两次提交中发生的变化。从 AE 的差异显示了 他们 他们的 两次提交中所做的更改。

Git 然后尝试合并 两组更改 并将其应用到提交 A 中的快照。如果 Git 认为这一切顺利,Git 将继续并根据结果创建一个新的快照——一个新的提交。通过接受我们的更改并添加他们的(或者,等效地,接受他们的更改并添加我们的),Git 的合并提交将具有作为其快照的?正确的?组合。这里的问号是因为 Git 只是使用简单的 line-by-line 规则。结果可能 not 在某种其他意义上是正确的:它只是 correct-by-Git's-rules.

无论如何,Git 的新 merge commit 现在将链接回 both 我们当前的提交 C 他们的提交 E:

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

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

但是,这会产生巨大的后果,因为 Git finds 提交的方式是从某个名称开始的——通常是分支名称,尽管任何类型的名称都可以做 - 并使用它来定位 last 提交,然后按照 all 向后链接到 all之前的提交。因此,从 F、Git “同时”返回到 CE6


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


变基

从根本上说,Rebase 是关于获取一些“okay-ish,但还不够好”的提交,然后 将它们复制 到 new-and-improved 提交(据说)更好,然后 放弃原件转而使用 new-and-improved 副本

这样做有几个问题:

  • Git“喜欢”添加新提交。它“不喜欢”丢弃旧的提交。 Rebase 强制 Git 将旧的替换为 new-and-improved 的,就目前而言这很好,但是 ...

  • 我们从一个发送提交Git 存储库到另一个。一旦它们被复制——一旦马离开谷仓并被克隆——摧毁其中的一些就没有好处了。如果我们有 new-and-improved 个替换,我们必须让 每个 Git 有 副本 的原件拾取并切换new-and-improved 替换。这意味着我们需要强制其他 Gits 放弃一些现有的提交。

一个总是有效的简单规则是:只替换你从未放弃的提交。这是有效的,因为如果你有唯一的副本,你的 new-and-improved 替换不会不需要得到任何 other Git 来扔掉旧的。不涉及其他 Git 存储库!但是太简单了,至少有很多GitHub work-flows.

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

无需深入了解所有细节,git rebase 所做的是:

  • 列出要复制的提交(哈希 ID);
  • 使用Git的detached HEAD模式以避免需要临时分支;
  • 检查副本所在的目标提交 go-after;
  • 复制 to-be-copied 提交,一个接一个,使用 git cherry-pick 或类似的东西;最后
  • 移动分支名称以指向最后复制的提交。

在这种情况下,您可以将两个现有提交变基(复制)为两个 new-and-improved 提交:

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

其中 B'C'BC 的副本。 B' 中的快照是通过对 E 中的快照进行 更改 构建的;要进行的更改是 比较 AB 所看到的更改。 C' 中的快照类似,但是通过将 B 更改为 C

复制完成后,Git 将旧的 main 标签从旧的 C 提交上剥离,并将其粘贴到新的 C' 提交上:

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

最初的 BC 提交仍然存在一段时间,但是如果没有简单的方法 找到 它们,您就不会 再见他们。如果您没有仔细记下原始 BC 的真实哈希 ID,您会认为它们的 new-and-improved 替换不知何故神奇地 改变了 BC 到位。但它们没有:它们 是全新的 ,并且旧的提交仍然存在。旧的提交只是未使用。一段时间后——默认至少 30 天——Git 将认为它们是垃圾,并最终用 git gc“垃圾收集”它们(Git 运行 自动你,通过 git gc --auto 从各种 Git 命令中分离出来,你无需做任何事情。

如果一切顺利,rebased 提交会“保留您工作的本质”,让您看起来好像在看到您的同事将要做什么之后开始工作.不过,复制的提交中的 date-and-time 标记更复杂:

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

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