将远程/branch_a拉入branch_a时,阻止 git 将branch_a合并到当前

  • 本文关键字:branch 合并 git 阻止 拉入 git
  • 更新时间 :
  • 英文 :


我正在编写一个脚本,将所有存储库更新到我们主开发分支上的最新代码。它基本上是:

git -C ./$project pull origin develop:develop

我不能确定运行脚本的人没有积极处理功能分支,也不能保证他们所在的分支是从开发分支出来的。因此,我只想将起源/发展拉入发展,仅此而已。没有更多的操作。

目前,在执行此操作时,git 将 development 拉入开发,然后尝试将开发合并到当前分支中。我不想要这最后一步。我搜索了文档以提取和获取,但没有找到任何可以提供帮助的东西。有没有办法做到这一点,而不必手动检查是否有更改、藏匿、弹出等。

TL;博士

根本不要这样做。 让人们直接使用origin/develop。 教他们如何以及为什么。 他们需要做的就是使用git fetch.

如果做不到这一点,请使用git fetch origin develop:develop并为某些人遇到问题做好准备。

git pull字面意思是:

  1. 运行git fetch; 然后
  2. 运行第二个 Git 命令。

您可以选择第二个命令是git merge还是git rebase但无论哪种方式,您都将根据您在步骤 1 中git fetch-ed 的内容影响当前分支。

根据您在此处的评论:

。我所拥有的,git pull origin develop:develop,这实际上在我feature/something时将发展拉入开发。但是,git THEN 尝试将开发合并到feature/something...

从技术上讲,git pull origin develop:develop不会将其(原产地)developpull到您的(本地)develop中,因为pull意味着fetch + second command。 它所做的只是将他们的develop带入您的develop- 这给了你一个答案:

git fetch origin develop:develop

请记住,大多数1git pull参数都直接传递给git fetch。 这是第一个命令在这里完成所有工作! 所以只需调用它。 但是这里有一些问题,所以请继续阅读。


1这里的例外情况是:

  • 特定于第二个命令的选项,以及
  • git pull本身"吃掉"的选项,例如指定要使用的第二个命令的选项。

可能出现什么问题?

git fetch命令意味着:

  • 调用其他一些 Git 存储库(在本例中为origin),使用存储在远程下的 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。 也就是说,您希望根据develop更新origin/develop,并且愿意跳过更新所有其他origin/*名称。阿拉伯数字或者你可以运行:

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

可能会失败。 因此,我们需要处理失败的情况,或者找到更好的方法来处理这个问题。

developgit worktree add添加的任何工作树中的当前分支时,会出现另一个问题,并且许多版本的 Git(从 Git 2.5 中添加git worktree add到 Git 2.35 发布)无法检测到这一点。 我们稍后会谈到这一点,但让我们先看看在正常(主)工作树中更新本地分支名称develop的问题。


2这里这样做的通常原因是为了让这个git fetch跑得更快。 这可能会使下一个获取所有内容(默认值)的git fetch变慢,因为该现在有更多要获取的内容。 因此,这是一种"现在付钱或以后付钱"的情况,事实证明,现在付钱通常比以后付便宜,因为总成本通常较低(减少开销,有时还有更好的压缩)。 但并非总是如此——"何时付款"是您可以自己决定的事情。

3如果您的网络出现故障,或者origin没有developgit fetch origin也可能失败。 但是在这两种情况下,我们在这里根本无法完成任何事情,所以我们不必担心它们。


参考资料和快进

developname 这样的分支是 Git 中的一种引用或引用形式。 像origin/develop这样的远程跟踪名称也是如此,事实上,像v1.2这样的标签和几乎所有其他名称也是如此,包括HEAD(尽管HEAD也被称为伪引用,因为它在 Git4中具有特殊的魔法属性)。 这个术语ref是"远程"之后的git fetch参数被称为refspecs 的原因:它们指定引用,位于存储库/存储库交互的"两侧",如fetchpush

在任何情况下,Git 中的每个分支或远程跟踪名称都是受约束的:它只包含一个提交哈希ID.5提交,在 Git 中,有一个特殊的属性:它们向后指向早期的提交。 这形成了一个竖立的无环图或DAG,DAG在提交之间创建一个部分顺序,因此给定任何一对提交

...--G--H   <-- alice

I--J   <-- bob

K--L   <-- carol

在这里,Bob 在 Alice 所做的之后添加了两个提交,然后 Carol 在此之后又添加了两个提交。 (较新的提交朝右,较旧的提交朝左。 我们可以从爱丽丝所在的地方前进,到鲍勃所在的地方,再到卡罗尔所在的地方。

另一方面,我们可以有这个:

I--J   <-- bob
/
...--G--H   <-- alice

K--L   <-- carol

在这里,如果我们是爱丽丝,我们可以向 Bob 前进两跳并最终到达提交J,或者我们可以向 Carol 前进两跳并最终到达L。 但是,一旦我们选择了两个前进动作中的一个,我们就无法再次前进以到达其他提交。 我们必须备份才能找到它们。

当我们遇到第二种情况时,我们在 Git 中经常做的是使用git merge组合工作。 当我们这样做时,Git 会生成这个作为我们的图:

I--J
/    
...--G--H      M
    /
K--L

我拿走了标签(分支名称),只留下了提交。 提交是 Git 关心的,但标签——分支名称——是我们让 Git为我们找到提交的方式,所以标签也很重要。 它们对 Git 来说并不重要,但对我们来说却很重要。

Git 存储库发生的情况是,如果我们自己在develop工作,我们可能会在origin上进行一两次尚未结束的提交:

I--J   <-- develop (HEAD)
/
...--G--H   <-- origin/develop

我们现在正在使用 - 使用 - 提交J。 同时,其他人可能会git push他们的两个提交来develop原点,一旦我们运行git fetch origin

我们得到:
I--J   <-- develop (HEAD)
/
...--G--H

K--L   <-- origin/develop

我们现在处于我上面画的鲍勃和卡罗尔的情况:我们必须回去才能前进,所以我们通常会做的是跑git merge

Git 的git fetch不运行git merge,但 Git 的git pull运行。这是区别的核心——或者至少是原始的核心,在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/developgit mergehash-of-L——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 的分支名称复制哈希 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。 例如,main9c897eef06ddbc07872e。 这意味着9c897eef06ddbc07872e的祖先。 但是在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

提交IJ仍然存在于我们的存储库中(使用上面三个点左侧的哈希 ID,我可以找到旧的),但名称origin/seen将不再找到这些。 它会找到L,找到K,找到H,等等,但它不会再找到JI了。

git fetch进行这种"强制更新"的原因是,git fetch更新远程跟踪名称的refspec中带有前导加号+。 前导加号是"力旗"。 它指示如果无法进行快进操作,Git 应继续并通过执行非快进强制更新来"丢失提交"。

HEAD、Git 的索引和工作树如何协调

在 Git 中工作时,您可以从存储库开始。 从本质上讲,这是一对数据库,其中一个(通常更大)保存提交和其他 Git 内部对象,另一个(通常小得多)保存名称("refs"或"references")。 引用从人类可读的名称转换为哈希 ID。 Git需要哈希 ID 才能在更大的数据库中查找对象。 Git 不需要名称(在任何技术意义上),但人类需要;因此,Git 提供了名称并以使用它们的方式使用它们。

较大的对象数据库中的内容都是只读的。 您可以更改存储在任何名称中的哈希 ID,但不能更改哈希 ID 给出的对象。 如果你做了一个错误的提交(我们都时不时地这样做),你可以用一个新的和改进的替代品来代替它,并且因为新的提交会添加,所以很容易从刚刚添加到的提交链的末尾弹出最后一个提交,并放置新的最后一个提交。 这就是git commit --amend真正工作的方式:旧的提交没有改变,它只是被完全弹出,只要没有人注意到原始哈希ID,并且只使用分支名称,没有人知道你首先做了错误的提交。

但是,由于每次提交中的所有内容都是完全只读的,因此我们遇到了问题。 从技术上讲,每次提交都存储每个文件的完整快照,但采用特殊的只读、仅 Git、压缩和删除重复的格式,只有 Git 才能读取。 这对于存档非常有用,但对于完成任何实际工作完全没用。

因此,除了存储库之外,Git 还为我们提供了一个工作树,6或简称工作树。 工作树只是您工作的地方。 你选择一些提交——通过它的哈希 ID,即使你使用分支名称让 Git 为你查找哈希 ID——并告诉 Git:我想在这个提交上工作。Git 将从该提交中提取文件并将它们放入您的工作树中。

你现在在工作树中拥有的是普通的日常文件。 计算机上的所有程序都可以读取和写入这些文件。 它们并不奇怪,Git化,重复数据删除的东西,甚至可能根本不是文件。7它们是文件。 只有一个大障碍:它们不在 Git 中。你的工作树文件可能来自 Git,但一旦出来,它们就只是文件,根本不是 Git 文件。

当然,最终,您可能希望对这些普通文件进行一些工作,并使用这些文件进行新的提交。 如果 Git 像大多数其他版本控制系统一样,您只需告诉 Git 进行新的提交,它就会自动检查每个工作树文件。 这是,或者可能是,非常缓慢和痛苦。8所以这不是 Git 所做的。

相反,Git 会保留每个"活动"文件的第三个副本或"副本"。 大多数版本控制系统有两个:在当前提交中有一个是只读的,还有一个,在你的工作树中,你正在处理/使用。 在 Git 中,还有第三个有点"介于"其他两个之间。 第三个 Git "副本"位于 Git 所说的索引暂存区域缓存中(目前很少见)。

我把"copy"放在这样的引号中,因为 Git 索引中的内容是压缩和去重复的格式。 它不像提交文件那样冻结:特别是,您可以批量替换它。 当你对工作树中的文件运行git add时,Git 将:

  • 阅读工作树副本;
  • 压缩它并查看是否有重复项;
  • 如果是重复项,请使用原始文件,扔掉压缩结果;如果不是,则压缩文件现在已准备好提交。

所以在git add之后,Git 已经准备好提交文件了。 在git add之前,Git ...准备好提交文件,采用当前提交的形式。 重复数据消除可以解决相同的事实。 如果将文件更改原来的样子并对其进行git add,则git add时间都会执行重复数据消除。 如果你把它改成全新的东西,它就不是重复的,现在有一个实际的副本。 因此,索引中的内容始终可以提交,并且已预先消除重复。 这就是git commit如此之快的原因:毕竟它不必准备一个全新的提交。 要进入此提交的所有文件都已预先打包;他们只需要一个快速冻结操作即可进入新的提交。

因此,当前提交、Git 的索引/暂存区您的工作树都协同工作。 Git 知道当前提交的哈希 ID。 Git 的索引中有文件,随时可以提交。 对你来说,你有你的工作树,你在那里做你的工作。

如果您决定处理当前提交,而是切换到其他分支和某个其他提交,请运行:

git checkout otherbranch

git switch otherbranch(自 Git 2.23 起)。 Git 从其索引和工作树中删除当前提交的文件。 它会在其索引和您的工作树中安装另一个提交的文件。 通过其文件重复数据删除技巧,Git 可以非常快速地分辨出哪些必须删除和替换的文件实际上是完全相同的,对于这些文件,它可以跳过所有工作,并git checkout非常快。

这里有一个很大的警告。 特殊文件HEAD——我们上面前面提到的伪引用——不包含当前的提交哈希 ID,至少当我们"在"分支上时不包含。 相反,它包含当前分支名称。 也就是说,如果我们在分支develop,文件HEAD只是说"分支开发"。 它是包含提交哈希 ID 的分支名称本身。 该提交哈希 ID 会导致 Git 索引和工作树中的存档快照,这就是 Git 知道在切换到另一个提交时要删除和替换哪些文件的方式。

问题是:如果HEAD包含分支名称,则无法更新该分支名称。这是因为名称包含哈希 ID,我们稍后需要该哈希 ID。

Git 中还有另一种模式,称为分离 HEAD模式。 在这里,HEAD字面上包含一个原始哈希 ID,而不是分支名称。 在此模式下,可以安全地更新任何分支名称,因为HEAD中没有分支名称。 但是我们仍然可以得到git worktree add问题:每个添加的工作树都必须包含自己的HEAD和索引(换句话说,每个工作树都有一个HEAD和索引)。

因此,无论我们是否"在">工作树中的某个分支上,都必须检查任何添加的工作树。 Git 版本 2.5 到(但不包括)2.35 无法执行此检查,因此如果您要偷偷摸摸,并且有人可能拥有此 Git 版本,则应自己执行此检查。


6所谓的存储库缺少工作树。 这意味着工作树中没有任何内容被签出(因为没有)。

7Git 的 blob 对象存储文件的内容;名称存储得很奇怪;一大堆对象可以挤在一起,Git 称之为包文件。 使用包文件时,您可能只有一个操作系统样式的文件(包文件)来保存所有源文件! Git 还有其他格式,因此它可以有效地工作;所有这些都隐藏得很好,不像有一个索引和一个工作树的事实。

8问问任何人,在 1980 年代或 1990 年代甚至更晚的时候,他们会在他们的系统中运行等效的 of-commit 动词,然后出去吃午饭,因为至少需要 15 分钟才能发生其他事情。 说真的,有时候只有一两分钟,但就是感觉真的很可怕,很慢,让人不愿意承诺。 当git checkoutgit commit只花了几秒钟时,我们都认为它一定是坏了。

计算机现在更快,并且使用SSD而不是3600 RPM旋转介质,存储也快得多,但现在项目通常更大,所以它有点平衡。


这为我们提供了故障案例列表

我们可以运行git fetch origin develop:develop. 这让我们的 Git 软件调用其他一些 Git 软件,无论存储在origin名称下的任何 URL,并与该软件协商以查看它们是否有一个名为develop的分支。 如果是这样,我们的 Git:

  • 从他们的 Git 中获取他们拥有的任何新提交,我们没有,我们需要更新我们的origin/develop;
  • 相应地更新我们的origin/develop,必要时强制更新;和
  • 尝试使用强制更新来更新我们的develop

如果出现以下情况,更新将失败:

  • 当前分支名为develop:这是上面描述的当前提交-获取-不同步问题;或者
  • 任何添加的工作树都在分支develop上,Git 版本是 2.35 或更高版本:它在 2.5 及更高版本中不会失败,但不包括 2.35,但这实际上更糟,因为这样添加的工作树现在是不同步的;
  • 更新不是快进。

如果没有人在使用git worktree add,则中间问题(迄今为止最糟糕的问题)不会发生,因此只有 Git注意到并拒绝的两个问题才会发生。 但它们实际上可能发生。 如果他们这样做,这意味着用户应该提交他们的工作并根据需要合并或变基(即,用户应该首先在这里使用git pull或等效项)。 如果有人正在使用git worktree add并且有一个添加的工作树,该工作树位于"分支develop上",他们应该在该特定添加的工作树中使用 git-pull-或等效进程。

为什么用户应该直接使用origin/develop

假设我们正在处理某个功能分支,该分支将在某个时候添加到其他存储库的develop中,并且我们应该根据需要重新设置功能分支,或者从其他存储库的开发合并到我们的功能分支中。 这些是正常的日常 Git 用法,我们偶尔需要更新我们的origin/develop.

但是我们可以随时通过运行来轻松更新origin/developgit fetch. 这可能什么也没做,或者快进我们的origin/develop,或者强制更新我们的origin/develop无论其中哪一种,我们的origin/develop现在都是最新的。我们根本不需要本地develop分支机构! 我们现在刚刚运行:

git rebase origin/develop

或:

git merge origin/develop

在必要和适当的情况下。

同样的工作方式也适用于main:我们根本不需要mainmaster分支。 我们可以只在自己的分支上工作,并直接使用origin/mainorigin/master

如果我们有理由查看由origin/mainorigin/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模式并不困难,真的,它只是一个皱纹,应该在需要时解决。

最新更新