TL;DR
根本不要这样做。让人们直接使用origin/develop。教他们如何以及为什么。他们需要做的就是使用git fetch。
如果做不到这一点,请使用git fetch origin develop:develop 并为某些人遇到问题做好准备。
长
git pull 字面意思是:
- 运行
git fetch;那么
- 运行第二个 Git 命令。
您可以选择第二个命令是git merge 还是git rebase,但无论哪种方式,您都会根据您在步骤1 中git fetch-ed 的内容影响当前分支。
基于your comment here:
...我所拥有的,git pull origin develop:develop,事实上,当我在feature/something 上时,这确实将发展拉向发展。但是,git THEN 尝试将 develop 合并为 feature/something ...
从技术上讲,git pull origin develop:develop 不 pull 他们的(来源)develop 进入您的(本地)develop,因为pull 表示fetch + second command。它所做的只是获取他们的develop 到你的develop——这会给你一个答案:
git fetch origin develop:develop
请记住,大多数1git pull 参数直接传递给git fetch。这是第一个命令在这里完成所有工作!所以只需调用它。但是这里有一些问题,请继续阅读。
1这里的例外是:
- 特定于 second 命令的选项,以及
- 被
git pull 自己“吃掉”的选项,例如用于指定要使用的第二个命令的选项。
可能出了什么问题?
git fetch 命令的意思是:
-
调用其他 Git 存储库(在本例中为 origin),使用存储在 remote: 下的 URL,这是其他 Git 存储库的简称。远程,如origin,只是Git 可以存储一些信息的名称:
- 获取和/或推送的 URL(您可以使用不同的推送 URL);
- 一些默认值和/或获取和/或推送的魔法;
- 其他(未指定:Git 保留在未来添加新内容的权利)。
-
让他们列出他们的分支和标签名称以及与这些名称相关的提交;
-
如果/根据需要/希望下载新的提交;和
-
根据前面的步骤,按照存储在遥控器名称下的一些魔术设置的指示,更新任何远程跟踪名称(在本例中为origin/* 名称)。
为此,git fetch 需要 遥控器的名称,例如 origin。你可以用一个来运行它:
git fetch origin
或没有:
git fetch
如果你不使用它运行它,Git 会猜测 使用哪个 遥控器。如果你只有一个遥控器——这是典型的情况;大多数存储库只有一个名为 origin 的遥控器——这是 Git 会猜测并因此使用的遥控器,因此您可以运行 git fetch 而无需任何参数。
添加了远程名称后,如git fetch origin,然后您可以继续列出在远程中看到的分支名称:
git fetch origin develop
例如。当你这样做时,你是在告诉你的 Git 软件,尽管他们的 Git 可能会列出十几个或一百万个分支名称,但你只对更新一个感兴趣,即develop .也就是说,您希望您的 origin/develop 根据他们的 develop 更新,并且您愿意跳过更新所有其他origin/* 名称。 2 或者你可以运行:
git fetch origin br1 br2 br7
从而更新您的origin/br1、origin/br2 和origin/br7。
这些附加参数中的每一个——它们绝对需要前面的origin,因为它们必须在遥控器之后出现;第一个参数将被假定为远程:git fetch br1 develop 表示“从远程 br1 获取 develop”,无论 br1 是否为分支名称——git fetch 称之为 refspec时间>。这是一种非常简单的 refspec,因为完整的 refspec 由四个部分组成:
- 一个可选的前导加号
+;
- 左手名,例如
develop;
- 右手名,如
develop;和
- 一个分隔冒号 (
:) 字符,用于分割左右两侧。
当您只写一个左侧名称时,您可以省略分隔符,因此git fetch origin develop 可以正常工作。但是,如果您要提供 右侧 的名称,则必须包含冒号。
当我们在这里使用冒号时,它告诉git fetch 它应该尝试在我们的 存储库中创建或更新我们的 名称之一。这可能会失败。特别是如果develop 是当前 分支,它将 失败,并且在其他几种情况下。因此:
git fetch origin develop
会起作用,3 但是:
git fetch origin develop:develop
可能会失败。所以我们需要处理失败的情况,或者找到更好的方法来处理。
当develop 是来自git worktree add 的任何添加 工作树中的当前分支时,会出现另一个问题,并且许多版本的Git(从git worktree add 被添加到Git 2.5直到 Git 2.35 发布)未能检测到这一点。我们稍后会谈到这一点,但让我们先看看在正常(主)工作树中更新本地分支名称 develop 的问题。
2这里这样做的通常原因是为了让 this git fetch 运行得更快。这可能会使 next git fetch 获取 everything(默认值)更慢,因为现在需要获取更多内容。所以这是一种“现在付款或以后付款”的情况,事实证明,现在付款实际上通常比以后付款更便宜,因为总成本通常更低(减少开销加上,有时,更好的压缩)。但并非总是如此——“何时付款”是您自己决定的事情。
3如果您的网络出现故障,或者没有develop on origin,git fetch origin 也可能失败。但是在这两种情况下,我们根本无法在这里完成任何事情,因此我们不必担心它们。 ?
引用和快进
像develop 这样的分支名称是 Git 中 reference 或 ref 的一种形式。像origin/develop 这样的远程跟踪名称也是如此,事实上,像v1.2 这样的标签以及几乎所有其他名称也是如此,包括HEAD(尽管HEAD 也被称为伪引用 因为它在 Git4 中具有特殊的神奇属性)。这个术语,ref,是在“远程”之后的 git fetch 参数被称为 refspecs 的原因:它们在存储库的“两侧”指定引用/存储库交互,例如 fetch 或 push。
在任何情况下,Git 中的每个分支或远程跟踪名称都受到限制:它只包含一个 commit 哈希 ID。5 在 Git 中,提交具有特殊的属性:它们向后指向到较早的提交。这形成了Drected Acyclic Graph 或 DAG,并且 DAG 在提交之间创建了一个偏序,因此给定任何一对提交 1, C2> 我们可以测试是否 C1 ≺ C2。有趣的花括号小于 ≺ 字符表示 precedes(还有相等和后继属性 ≻ 所以我们有一套完整的 ≼ etc 操作——但事实上这是一个 偏序表示 C1 ⊀ C2并不意味着 C1 ≽ C2:它们可能根本没有任何定义的顺序)。
好的,所以有一些数学(我被保证不会有数学!no you weren't),但我们不需要在这里详细说明:它到底是什么意味着 有时一个分支以一种简洁明了的方式“向前移动”,而有时则不然。这是一个简单的例子:
...--G--H <-- alice
\
I--J <-- bob
\
K--L <-- carol
在这里,Bob 在 Alice 所做的操作之后添加了两个提交,然后 Carol 在此之后又添加了两个提交。 (较新的提交在右侧,较旧的提交在左侧。)我们可以从 Alice 所在的位置前进到 Bob 所在的位置,再到 Carol 所在的位置。
另一方面,我们可以这样:
I--J <-- bob
/
...--G--H <-- alice
\
K--L <-- carol
在这里,如果我们是 Alice,我们可以向 Bob 前进两跳并最终到达提交 J,或者我们可以向 Carol 前进两跳并最终到达 L。但是一旦我们选择了两个向前移动中的一个,我们就不能再次向前 去进行其他提交。我们必须备份才能找到他们。
当我们遇到第二种情况时,我们在 Git 中经常做的就是使用git merge 来组合工作。当我们这样做时,Git 会生成这个作为我们的图表:
I--J
/ \
...--G--H M
\ /
K--L
我删除了 labels(分支名称),只留下了 commits。提交是 Git 关心的,但标签(分支名称)是我们让 Git 找到提交为我们的方式,因此标签也很重要。它们对 Git 来说并不重要,但对我们来说却很重要。
Git 存储库的情况是,如果我们自己正在处理 develop,我们可能会进行一两次尚未在 origin 上完成的提交:
I--J <-- develop (HEAD)
/
...--G--H <-- origin/develop
我们现在正在使用 - 与 - 提交 J。同时,其他人可能 git push 他们对 develop 的两次提交在 origin 上,一旦我们运行 git fetch origin 我们得到:
I--J <-- develop (HEAD)
/
...--G--H
\
K--L <-- origin/develop
我们现在处于我上面描绘的 Bob-and-Carol 的情况:我们必须回去才能继续前进,所以我们通常会运行 git merge。
Git 的 git fetch 不能运行 git merge,但 Git 的 git pull 可以。 这就是区别的核心——或者至少是最初的核心,在 pull 变得复杂之前——增加一个 rebase 选项——在 fetch 和 pull 之间。这在这里真的很重要,因为有时 git merge 的情况要容易得多。假设我们在develop,但我们还没有自己做出任何新的提交,所以我们有:
...--G--H <-- develop (HEAD), origin/develop
然后我们运行git fetch,它获得新的提交(我将再次称它们为K-L,跳过I-J;提交的实际“名称”是大而难看的随机哈希ID,我们只是在使用让我们虚弱的大脑保持简单的字母):
...--G--H <-- develop (HEAD)
\
K--L <-- origin/develop
如果我们现在运行 git merge 并为其提供正确的内容,以便它合并提交 L——例如,git merge origin/develop 或 git merge <em>hash-of-L</em>——Git 注意到这个特定的合并是微不足道的。我们实际上还没有完成任何 Git 需要合并的工作,因此 Git 可以进行快进,而不是进行艰苦的工作,从而产生以下效果:
...--G--H
\
K--L <-- develop (HEAD), origin/develop
git merge 执行的快进操作而不是合并发生在当前提交的合并基础和目标提交是当前提交。 Git 将此称为快进合并,因为我们最终会在工作树中签出提交 L,同时将名称 develop 像这样向前移动。
现在,git fetch 可以对它想要更新的任何名称执行非常相似的快进操作。通常我们有git fetch 更新我们的远程跟踪名称,这些名称以快进方式移动是非常典型的。 (从技术上讲,这意味着远程跟踪名称在git fetch 之前找到的“之前”提交在“之后”提交之前。在内部,Git 具有整个 C1 ≼ C2 测试机制来决定是否可以进行快进。)
4特别是,.git/HEAD(目前至少)始终是一个文件,如果文件由于某种原因被破坏,Git 将不再相信存储库是一个存储库。如果您在更新分支时计算机崩溃,则可能会发生这种情况。幸运的是,其中一些案例很容易恢复——但这是另一个问题的主题。
5每个 ref 只保存一个哈希 ID,但一些 ref,例如标签名称,可以保存非提交哈希 ID。由于远程跟踪名称是通过从其他 Git 的 branch 名称复制散列 ID 生成的,并且分支名称被限制为保存提交散列 ID,因此远程跟踪名称也受到类似限制。
非快进情况
有时快进是不可能的。例如,如果有人在分支上使用 git rebase,而您使用 git fetch 更新获取他们的新提交,您将看到,例如:
+ 6013c4a515...94929fa71c seen -> origin/seen (forced update)
git fetch 的实际输出是:
[messages about enumerating and counting and compressing, snipped]
From <url>
9c897eef06..ddbc07872e master -> origin/master
9c897eef06..ddbc07872e main -> origin/main
e54793a95a..dc8c8deaa6 maint -> origin/maint
c6f46106ab..0703251124 next -> origin/next
+ 6013c4a515...94929fa71c seen -> origin/seen (forced update)
7c89ac0feb..4d351f5272 todo -> origin/todo
* [new tag] v2.37.0-rc0 -> v2.37.0-rc0
* [new tag] v2.37.0-rc1 -> v2.37.0-rc1
请注意大多数 分支更新如何仅打印两个由两个点分隔的提交哈希 ID。例如,main 从 9c897eef06 变为 ddbc07872e。这意味着9c897eef06 是ddbc07872e 的祖先。但是在seen(我的origin/seen)上,一些提交已被删除并替换为新的和改进的提交。所以那个特定的git fetch 输出行:
- 以
+为前缀;
- 包含三个点而不是两个点;和
- 已附加
(forced updated)。
所有这三个都告诉我们同样的事情:这不是快进操作。 Git 告诉我们三遍,因为知道这一点非常重要。 (然而很多人在这里从来没有注意过。??)非快进更新需要一定的额外力量,因为它特别“丢失”了分支末尾的提交。也就是说,我们有:
I--J <-- origin/seen
/
...--G--H
\
K--L <-- where the `git fetch` is told to make `origin/seen` go
强制更新后,我们有:
I--J [abandoned]
/
...--G--H
\
K--L <-- origin/seen
提交I 和J 仍然存在在我们的存储库中(使用上面三个点左侧的哈希ID,我可以找到旧的),但是name origin/seen 将不再找到这些。它会找到L,它会找到K,它会找到H,依此类推,但它不会再找到J或I。
使git fetch 做这种“强制更新”的原因是具有git fetch更新远程跟踪名称的refspec具有前导加号@987654465 @ 在里面。那个领先的加号是“强制标志”。它表示如果 不可能 进行快进操作,Git 应该继续并通过执行非快进强制更新来“丢失提交”。
HEAD、Git 的索引和你的工作树如何协调
在 Git 中工作时,您从存储库开始。从本质上讲,这是一对数据库,一个(通常大得多)保存提交和其他 Git 内部对象,一个(通常小得多)保存名称(“refs”或“references”)。 refs 从人类可读的名称转换为哈希 ID。 Git 需要哈希 ID 来在更大的数据库中查找对象。 Git 并不需要名称(在任何技术意义上),但人类需要;正因为如此,Git 提供了名称并以它使用它们的方式使用它们。
更大的对象数据库中的内容都是只读的。您可以更改存储在任何名称中的散列 ID,但不能更改散列 ID 给出的 object。如果您做出了错误的提交(我们都时不时地这样做),您可以为它进行新的和改进的替换,并且因为新的提交添加,这很容易将 last 提交从刚刚添加的提交链的末尾弹出,并将 new 最后一次提交放在其位置。这就是git commit --amend 真正的工作原理:旧的提交没有改变,它只是被完全弹出,只要没有人注意到原始哈希 ID,并且只使用分支名称,没有人知道你在第一次提交时做出了错误的提交地点。
但是,因为每次提交 in 中的所有内容都是完全只读的,所以我们遇到了问题。从技术上讲,每个提交都存储 每个 文件的完整快照,但采用特殊的、只读的、仅限 Git 的、压缩的和 去重复 格式,只有 Git 可以读取.这对于存档非常有用,而且对于完成任何实际的工作完全没用。
因此,除了存储库本身,Git 还为我们提供了一个工作树、6 或简称工作树。工作树只是您工作的地方。你选择一些提交——通过它的哈希 ID,即使你使用分支名称让 Git 为你查找哈希 ID——然后告诉 Git:我想在这个提交上使用 /。 Git将从该提交中提取文件并将它们放入您的工作树中。
您现在在工作树中拥有的是普通的日常文件。您计算机上的所有程序都可以读取和写入这些文件。它们并不奇怪、Git 化、重复数据删除甚至可能根本不是文件。7它们是文件。只有一个大问题:它们不在 Git 中。您的工作树文件可能已经 来自 Git,但一旦出来,它们只是文件,而不是 Git文件。
当然,最终,您可能希望对这些普通文件进行一些工作,并使用这些文件进行 new 提交。如果 Git 与大多数其他版本控制系统一样,您只需告诉 Git 进行新的提交,它会自动检查每个工作树文件。这会或者可能会非常缓慢和痛苦。8 所以这不是 Git 所做的。
相反,Git 保留每个“活动”文件的第三个副本或“副本”。大多数版本控制系统都有两个:一个是只读的,在当前提交中,还有一个是你正在处理/使用的,在你的工作树中。在 Git 中,还有第三个“介于”其他两个之间。第三个 Git “副本”位于 Git 所称的索引、暂存区,或者(现在很少见)缓存 .
我将“复制”放在这样的引号中,因为 Git 索引中的内容是压缩和去重复的格式。它不会像提交的文件那样被冻结:特别是,您可以批量替换它。当您在工作树中的文件上运行 git add 时,Git 将:
- 读取工作树副本;
- 压缩一下看看有没有重复的;
- 如果是重复的,就用原来的,扔掉压缩后的结果;如果没有,压缩文件现在可以提交了。
所以在git add 之后,Git 准备好提交文件。在git add 之前,Git ... 已经准备好提交文件,以它在当前提交中的形式。重复数据删除处理的是相同的事实。如果您将文件返回 更改为原来的状态并git add,则重复数据删除将在git add 时间发生。如果你把它改成全新的东西,它就不是重复的,现在有一个实际的副本。因此,索引中的内容随时可以提交,并且是预去重。这就是使git commit 如此之快的原因:毕竟它不必准备一个全新的提交。所有要进入这个提交的文件都已经预先打包好了;他们只需要快速冻结操作即可进入新的提交。
因此,当前提交、Git 的index / staging-area 和您的工作树 都是协调的。 Git 知道当前提交的哈希 ID。 Git 的索引中有文件,随时可以提交。对你来说,你有你的工作树来做你的工作。
如果您决定不处理当前提交,而是切换到其他分支和其他提交,则运行:
git checkout otherbranch
或 git switch otherbranch(从 Git 2.23 开始)。 Git 从它的索引和你的工作树中删除 current 提交的文件。它将 other 提交的文件安装到它的索引和您的工作树中。通过其文件重复数据删除技巧,Git 可以非常快速地判断哪些文件必须删除和替换实际上完全相同,并且对于这些文件,它可以跳过所有工作 ,并让git checkout 变得非常快。
这里有一个很大的警告。特殊文件HEAD——我们前面提到的伪引用——不包含当前提交哈希ID,至少当我们“在”一个分支时不包含。相反,它包含当前分支名称。也就是说,如果我们在分支develop,文件HEAD 只会说“分支开发”。 分支名称本身包含提交哈希 ID。该提交哈希 ID 指向 Git 索引和工作树中的存档快照,这就是 Git 知道在您切换到另一个提交时要删除和替换哪些文件的方式。
问题出在:如果 HEAD 包含分支名称,我们无法更新该分支名称。这是因为 name 包含哈希 ID,而我们稍后需要该哈希 ID。
Git 中有另一种模式,称为 detached HEAD 模式。这里,HEAD 字面上包含一个原始哈希 ID,而不是一个分支名称。在这种模式下,更新任何分支名称都是安全的,因为HEAD 中没有分支名称。但是我们仍然会遇到git worktree add 的问题:每个添加的工作树都必须包含自己的HEAD 和索引(换句话说,每个工作树都有一个HEAD 和索引)。
因此,无论我们是否“在”main 工作树中的某个分支,也必须检查任何添加 工作树。 Git 版本 2.5 到(但不包括)2.35 无法执行此检查,因此如果您要偷偷摸摸,并且有人可能拥有此 Git 版本,您应该自己进行此检查。
6所谓的 bare 存储库缺少工作树。这意味着在工作树中从未检查过任何内容(因为没有)。
7Git 的 blob 对象 存储文件的内容;名称存储得很奇怪;并且可以将一大堆对象压缩到 Git 所谓的 pack 文件 中。使用包文件时,您可能只有一个操作系统风格的文件(包文件),其中包含所有源文件! Git虽然有其他格式,所以它可以有效地工作;所有这些都被很好地隐藏了,不像有一个索引和一个工作树。
8询问任何人,在 1980 年代或 1990 年代甚至更晚的时候,会在他们的系统中运行等效的提交动词然后出去吃午饭,因为它至少是 15在其他任何事情发生之前的几分钟。说真的,有时只有一两分钟,但感觉真的很可怕,很慢,让人不愿意犯。当git checkout 和git commit 只用了几秒钟,我们都认为它必须被打破。
现在的计算机速度更快,使用 SSD 而不是 3600 RPM 旋转介质,存储速度也快得多,但现在项目通常更大,所以有点平衡。
这为我们提供了失败案例列表
我们可以运行git fetch origin develop:develop。这让我们的 Git 软件调用其他 Git 软件,无论 URL 存储在名称 origin 下,并与该软件协商以查看它们是否有一个名为 develop 的分支。如果是这样,我们的 Git:
- 从他们的 Git 中获取他们有的任何新的提交,我们没有,我们需要更新我们的
origin/develop;
- 相应地更新我们的
origin/develop,必要时强制更新;和
- 尝试使用非强制更新来更新我们的
develop。
如果出现以下情况,更新将失败:
- 当前分支命名为
develop:这就是上面描述的current-commit-gets-desynchronized问题;或
- 任何添加的工作树都在分支
develop 上,并且 Git 版本是 2.35 或更高版本:它不会在 2.5 及更高版本中失败,但不包括 2.35,但实际上更糟因此,添加的工作树现在已不同步;或
- 更新不是快进。
如果没有人使用git worktree add,则不会出现中间问题(迄今为止最糟糕的),因此只会出现 Git 将注意到并拒绝的两个问题。但它们实际上可以发生。如果他们这样做了,这意味着用户无论如何都应该提交他们的工作并酌情合并或变基(即,用户首先应该使用git pull 或等效项)。如果有人正在使用git worktree add 并且添加了一个“打开”分支develop 的工作树,他们应该在该特定添加的工作树中使用 git-pull-or-equivalent 进程。
为什么用户应该直接使用origin/develop
假设我们正在处理某个功能分支,该功能分支将在某个时候添加到其他存储库的develop,并且我们应该根据需要重新设置我们的功能分支,或者从其他存储库的 develop 合并到我们的功能分支。这些是我们偶尔需要更新 origin/develop 的日常 Git 用法。
但我们可以通过运行git fetch 来轻松更新origin/develop,随时。这可能什么都不做,或者快进我们的origin/develop,或者强制更新我们的origin/develop。 不管是哪一个,我们的origin/develop 都是最新的。我们根本不需要本地的develop 分支!我们现在运行:
git rebase origin/develop
或:
git merge origin/develop
根据需要和适当。
同样的工作方法适用于main:我们根本不需要main 或master 分支。我们可以在自己的分支上工作,直接使用origin/main 或origin/master。
如果我们有理由查看 origin/main 或 origin/develop 或其他指定的提交,我们可以运行:
git checkout origin/develop
我们将处于“分离 HEAD”模式,使用所需的提交。然后我们:
git checkout feature/ours
重新开始我们的功能。或者,如果我们喜欢 git switch——它比 git checkout 更易于使用和更安全——我们将运行:
git switch --detach origin/develop
git switch 命令要求--detach 标志,因为 Git 新手通常不了解“分离 HEAD”模式是什么。分离的 HEAD 模式并不困难,真的,它只是一个需要解决的问题。