我分叉了一个存储库,然后创建了一个名为patch1
的分支进行更改并提交它们,然后我在上游创建了一个拉取请求并将其与 master 合并到上游。但是在我的本地存储库中,分支没有合并,如何从上游拉取合并而不从上游获取我不想要/不需要的其余分支,例如Hotfix12
或NewFeature5
?
这里的标准做法是什么?
由于上游 PR 更改了目标分支,因此您只需git pull
该分支即可。
我实际上有一个脚本,它为本地存在的每个分支执行git pull
,但是如果没有它,我会这样做(假设您从feature42
PR到master
):
git checkout master
git pull
如果您的上游 PR 也删除了源分支,您可能还希望在本地执行此操作:
git branch -d feature42
(首先,附带说明:我认为标准实践不一定那么有趣,因为你的 Git 存储库是你的。 你可以做任何对你有用的事情!因此,标准做法只有在对您有用时才有趣。 您可能需要尝试几种不同的方法。
TL;博士
我建议运行:
git fetch upstream
git checkout desired-branch
git merge --ff-only upstream/theirbranch # or upstream/branch, or upstream/master
(您可以按任一顺序进行提取和结帐)。 如果你真的喜欢git pull
,你可以让git pull
运行获取和合并,但我不喜欢git pull
,更喜欢分两个或多个步骤来做到这一点。 (我也有merge --ff-only
的别名,git mff
。
您现在可以自由删除您为完成此 PR 而创建的任何或所有名称,无论是在您自己的笔记本电脑 Git 存储库还是在您的 GitHub 分支中。 这些名称几乎不使用磁盘空间,但它们将使用"头部空间"(精神能量)来跟踪它们,因此我建议删除它们。
在某些情况下,此--ff-only
合并会失败;在这些情况下,请参阅下面的详细讨论。
长
记住这些关于 Git 的事情:
Git 是关于提交的。 Git 不是关于文件,甚至不是关于分支,而是关于提交。 提交存储数据(文件的快照)和元数据(例如谁以及何时制作它们)。所有提交都是100% 只读的:任何提交的任何部分都不能更改。 提交的真实名称是一个丑陋的哈希 ID,并且该哈希 ID 对存储在提交中的每个数据和元数据都非常敏感,因此实际上不可能更改提交的内容:如果你取出一个,稍微修改一下,然后放回去,你得到的是一个新的、不同的提交,带有一个新的和不同的哈希 ID。
分支名称(如
master
和develop
等)很有用,因为它们可以让您查找提交。 每次提交的真实名称是一个人类无法记住的大丑陋哈希ID。 但是我们不必记住一个丑陋的大哈希ID,因为我们有一台计算机可以记住它,在一个名字下!分支这个词是模棱两可的。 每当有人谈论某个 Git 分支时,请确保您知道他们的意思是分支名称还是其他内容。 这仅与您在这里的问题间接相关,但值得始终记住。 参见 我们所说的"分支"到底是什么意思? 一般来说,分支这个词是否意味着分支名称,还是一些提交集合,以分支名称指定的一个结尾,应该是显而易见的。有些人也用它来表示远程跟踪名称(我在这里尽量不这样做)。
您的仓库就是您的。 您有自己的分支名称。 您的分支名称不是任何其他 Git 的分支名称。 你的 Git 和他们的 Git真正共享的是提交,通过他们的哈希 ID。 (由于提交是 100% 只读的,如果你的 Git 和他们的 Git 可以真正共享提交,那很好。 如果没有,您的 Git 和他们的 Git 可以有单独的副本。 正如我们已经指出的,副本无法更改。
除了分支名称之外,Git 还有更多方法(即更多名称)可以记住任何一个特定提交的哈希 ID。 其中一种名称是远程跟踪名称,例如
origin/master
。 远程跟踪名称是 Git对其他某个 Git分支名称的内存(以及它们存储在该分支名称中的哈希 ID)。
最后两项是处理您的情况的关键。
我分叉了一个回购...
这意味着您使用了某个托管提供商(例如 GitHub)基于第一个 Git 存储库制作了第二个 Git 存储库。 也就是说,在 GitHub 端,您制作了一个克隆。 您的 GitHub 克隆现在独立于他们的 GitHub 克隆。
您可能还克隆到您自己的计算机(笔记本电脑或其他计算机)上,因此此时可能存在三个克隆。 没关系! 在 Git 世界中,你得到一个克隆,他们得到一个克隆,每个人都得到一个克隆! 有很多克隆没有问题...好吧,除了一个:每个克隆都有自己的分支名称。 这可能是很多要管理的分支名称。
GitHub 特别制作的克隆有一些特殊之处,当您使用"分叉此存储库"点击 Web 按钮时。 事实上,有几个特殊的东西,但这里重要的是:这个克隆将所有分支名称从你分叉的仓库复制到你的 GitHub 克隆。 您的 GitHub 克隆只有分支名称,没有远程跟踪名称。
如果您随后运行:
git clone <github-url>
要将分叉复制到笔记本电脑上的新克隆,第三个克隆没有复制所有"他们的"分支名称。 但请稍等片刻:他们是谁?
我们已经说过,GitHub上有两个有趣的克隆。它们在这里的含义取决于您使用的 URL。 如果您使用了原始仓库的 URL,则在进行分叉之前,"它们"是原始仓库。 如果您使用了分叉的网址,则"他们"就是您的分叉。
如果您刚刚分叉了他们的仓库,那么您的分叉具有与其分叉相同的所有分支名称(和存储的哈希 ID 值)和所有相同的提交(具有唯一的哈希 ID)。所以从某种意义上说,你克隆了哪一个并不重要。 但是随着时间的推移,你的分叉和他们的分叉可能会分开,因为你和/或他们向你的和/或他们的仓库添加了更多的提交。 如果您和他们添加了不同的提交,或者以不同的方式更新您和他们的分支名称,那么它开始变得重要。
通常,此时您要做的是在笔记本电脑上的克隆中创建两个Git 调用的远程。远程只是一个短名称,例如origin
,我们将让我们的(笔记本电脑)Git 存储其他一些 Git 存储库的 URL。 当你运行git clone <url>
时,你的 Git 创建了这个标准origin
远程。 由于 GitHub 上有两个有趣的存储库——你的分叉和它们的分叉——你可能很想添加第二个远程,这样你每个分叉都有一个远程。 第二个遥控器的标准名称是upstream
。 (这不是一个特别好的名字,因为 Git 中的其他几个东西在不同的时间被调用上游,但它足够常见,所以我们在这里使用它。
远程跟踪名称
让我们回到这样一个事实,即您的笔记本电脑端克隆没有将任何一个分叉的分支名称复制到您的笔记本电脑克隆的分支名称,并查看为什么 GitHub"分叉"按钮确实将其所有分叉的分支名称复制到您的分叉。 这一切都与远程跟踪名称有关。1你的笔记本电脑 Git 为你的笔记本电脑 Git 在远程 Git 中看到的每个分支名称创建远程跟踪名称。 这些远程 Gits 在您的笔记本电脑上具有名称:origin
和upstream
. 因此,您的笔记本电脑 Git 可以将这些名称粘贴在分支名称前面,并将 GitHub Gits 的master
(几乎可以肯定其中有两个)转换为origin/master
和upstream/master
。 它将GitHub Gits的develop
变成了origin/develop
和upstream/develop
。 这将对每个远程中的每个分支名称重复。
保存所有这些额外名称的成本非常低:它基本上根本不占用磁盘空间。 这是因为 Git 是关于提交的,而提交具有哈希 ID。 假设origin/master
说commit a1234567...
,upstream/master
说commit a1234567...
。 你自己的 Git 已经有提交a1234567...
,所以你的 Git 只需要存储一些名称-值对:origin/master=a1234567...
、upstream/master=a1234567...
。
那么,远程跟踪名称的好处是:
它们基本上根本不占用空间。 (Git 通常将它们存储在
.git/packed-refs
中,这是一个带有记录的单个文件,而不是多个文件,因此它们往往比磁盘块占用的时间更少。 您自己的分支名称在存储空间方面已经很便宜了,因为其中大多数都存储在单个磁盘块中,但这些甚至更便宜。它们会自动更新。 当你运行
git fetch origin
时,你的 Git 在origin
调用 Git(你在 GitHub 上的分支)。 您的 Git 从他们的 Git 获取所需的任何新提交和其他对象,然后更新您的所有(笔记本电脑)origin/*
远程跟踪名称以匹配您的所有(GitHub-fork)分支名称。 当你运行git fetch upstream
时,你的 Git 在upstream
调用 Git(他们在 GitHub 上的分叉)。 您的 Git 从他们的 Git 获取所需的任何新提交和其他对象,然后更新所有upstream/*
远程跟踪名称以匹配其所有分支名称。
你可能希望将--prune
添加到git fetch
命令中,或在 Git 配置中将fetch.prune
设置为true
,以便 Git 从远程跟踪名称中删除"他们的"Git(你或他们在 GitHub 上的分支)不再具有的任何分支名称。 如果没有--prune
,上面步骤 2 中的更新永远不会注意到他们(无论他们是谁)删除了feature/tall
,因此您的origin/feature/tall
或upstream/feature/tall
(无论它是什么)都会作为过时的远程跟踪名称徘徊。 使用--prune
或fetch.prune
,您的笔记本电脑 Git 注意到此名称应该消失,并将其删除。
那么:为什么GitHub的"分叉仓库"按钮没有创建远程跟踪名称而不是分支名称? 好吧,只有GitHub才能真正回答这个问题;但如果他们有,你需要一些方法来操纵GitHub上的远程跟踪名称。 由于他们没有,他们只需要为您提供一种在 GitHub 上操作分支名称的方法。 请注意,GitHub 没有用于获取的点击按钮:您无法让您的 GitHub 分叉运行git fetch
! 由于您在笔记本电脑上使用git fetch
来更新远程跟踪名称,因此 GitHub 上缺少fetch
意味着您无法在那里更新远程跟踪名称。
1从历史上看,远程跟踪名称实际上是在导致所有这些的各种决定之后出现的,但我认为以另一种方式遵循逻辑更有意义。
传输提交:git fetch
和git push
有两种常见的方法可以将提交导入 Git 存储库。 我们在上面已经提到了其中之一,即git fetch
. 你运行git fetchremote
,你的 Git 从远程名称中找出存储的 URL(例如,origin
的 URL),并在该位置调用 Git。
该 Git 为您的 Git 列出了它的所有分支名称(以及标签名称和其他内部名称,但在这里我们只真正查看分支名称)。 每个分支名称标识一个提交,这是分支的提示。 可以在该分支上访问的所有早期提交都可以使用该分支名称进行访问。 有关可访问性概念的全面讨论,请参阅像 (a) Git 一样思考。 了解可访问性是使用 Git 的关键,因此,如果概念不熟悉,您绝对应该完成这些内容。
此时,你的 Git 可以向他们的 Git 请求你的 Git 想要或需要但没有的任何提交和其他内部 Git 对象。 这一步实际上非常有趣,并且涉及很多图论,但我们可以理所当然地认为两个 Git 做得很好。 他们找出他们的 Git 拥有的一组相当小的 Git 对象,你的 Git 想要这些对象。 它们压缩这些对象——这就是这里所有counting objects
和compressing objects
消息的内容——并将它们发送过来。 您的 Git 将这些放入您的集合中,将提交和其他内部对象添加到笔记本电脑上的存储库中。 这允许 Git 更新您的远程跟踪名称:您现在拥有他们拥有的所有提交,以及您尚未提供给他们的任何提交。
请注意,您的远程跟踪名称实际上是为其 Git 预先保留的。您不会将自己的任何分支称为origin/master
或origin/develop
之类的。阿拉伯数字因此,Git 可以自由地粉碎和替换您的任何或所有远程跟踪名称:您的任何分支名称都不会受到影响。
如果你想走另一条路,fetch 的反面是推送。3但这里有一种不对称性。 当你运行git push originbranch
时,你会让你的 Git 再次通过查找远程的 URL 来调用其他一些 Git。 但是这一次,你的 Git 不是让他们列出他们的分支名称等并将提交带到你的Git,而是向他们发送提交和其他内部 Git 对象。 您向他们发送他们需要的任何提交,以使您自己的分支的提示提交branch
有用 - 这包括您拥有的任何可访问的提交,他们没有 - 我们再次获得所有计数和压缩对象消息。 但是现在,在将任何所需的提交发送到他们的Git 后,您的 Git 通常会礼貌地询问他们应该将他们的分支名称branch
设置为提交的哈希 ID,这也是您的分支branch
的提示。
他们没有设置远程跟踪名称! (特别是GitHub甚至没有远程跟踪名称。 他们不会设置其他保留空间名称。 他们设置了自己的分支名称。
当你的 Git 提出礼貌的请求时,如果他们不喜欢它,他们会拒绝这个请求。 如果您要创建新的分支名称,他们通常会喜欢。 但是,如果要更新现有哈希 ID,如果新的哈希 ID 引用的提交无法访问相同名称的先前哈希 ID,则他们不会喜欢更新。
也就是说,考虑一些提交链:
...--G--H <-- branch
现在我们将在末尾添加一些新的提交:
...--G--H <-- branch
I--J
并建议他们将自己的名字branch
从H
移至J
. 如果他们这样做,提交H
仍然是可以实现的:从J
开始并向后工作,我们从J
到I
,然后到H
。 因此,此请求将被接受。 但是,如果我们这样做:
...--G--H <-- branch
K--L
并要求他们将自己的名字设置为指向L
branch
,他们会拒绝,因为无法从L
H
联系到。 来自L
的可访问提交是L
,然后是K
,然后是G
,然后是G
之前的其他提交。
Git 对此的术语是,对名称branch
的更改必须是快进的。 将branch
从H
移动到J
是一种快进;将branch
从H
移动到L
是一种非快进。四
2从技术上讲,你可以。 Git 有内部命名空间,因此Git可以保持一切正常。 不过这不是一个好主意:你可能没有这些内部命名空间,你会把它搞砸。😀
3不是推拉,而是推拉!这是一个历史事故,我认为它导致了很多混乱,但它就是这样。
4要强制推送非快进更新,您可以使用--force
标志,或将+
标志添加到refspec中,这是我们在这里没有定义的东西。 这两种方法都将礼貌请求更改为命令。 他们仍然可以拒绝,但我们不会在这里担心这些细节。
拉取请求
拉取请求 (PR) 本身是特定于主机提供程序的功能。 Git 没有拉取请求! (Git 有一个git request-pull
命令,但它的作用是生成电子邮件。 请注意,如果我们拥有一个 GitHub 分叉,我们可以git push
它。 这一切都很好:我们可以更新我们的分叉。 如果我们的git push
运营是快进的,它们就会成功,在特殊情况下,即使不是快进,我们也git push --force
使我们的运营成功。 因此,我们可以将我们想要的所有内容git push
到我们的GitHub分支中,我们称之为origin
。 这让我们可以随心所欲地改变 GitHub 分叉的形状。 我们的分支将存储提交,就像任何 Git 存储库一样。 它将将它们存储在分支名称下,就像任何 Git 存储库一样。 它没有远程跟踪名称——这些名称特定于我们的笔记本电脑 Git 存储库——但这很好:我们不需要我们的分支有远程跟踪名称。
但是我们可能希望将我们的提交放入不属于我们的 GitHub 分支中,位于我们以upstream
的名称存储的 URL。 我们将如何做到这一点?
如果- 这是一个很大的问题,如果这通常不是真的 - 另一个GitHub分支的所有者授予我们对他们的存储库的写入权限,我们可以直接git push
我们的提交到upstream
。 但他们必须真正信任我们的存储库。
GitHub可以提供某种特殊的名称空间:半保护的分支名称模式,upstream
分叉的所有者可以给我们写,他们不会自己使用。 GitHub可以有一个执行机制来实现所有这些工作。 但他们没有。 相反,GitHub 给了我们拉取请求。
在我们进行 PR 之前,我们首先在origin
git push
将笔记本电脑制作的提交到我们自己的 GitHub 分支中。 这些提交通过其哈希 ID 进入,根据我们的git push
命令更新 GitHub分支中的分支名称。 最终,在我们的 GitHub 分支中,我们有一些分支名称,指向我们喜欢的一些提示提交,我们希望将其提供给操作我们称之为upstream
的 GitHub 分支的人。
正是在这一点上,我们发出了拉取请求。 我们使用 GitHub 的接口将提交从我们的 GitHub 分支发送到他们的 GitHub 分支(好像通过git push
),但它们upstream
以 GitHub 人员控制的特殊名称显示在分支中。5/sup>除了单击"制作 PR"按钮外,我们在这个过程中没有任何代理机构:GitHub 决定特殊名称,并为 PR 创建名称。 然后,GitHub 还会发送电子邮件、Slack 消息等——任何合适的消息——来提醒运行我们称之为upstream
的分叉的人,他们有一个新的拉取请求。
现在一切都取决于他们。
5这些是refs/pull/*
命名空间。 这个命名空间中的东西是有编号的:每个 PR 或问题在 GitHub 存储库中获得一个唯一的计数编号,当我们创建一个新的 PR 时,GitHub 给它一个编号——比如说123
具体——并创建表单的名称refs/pull/123/head
也许,也可能不是,refs/pull/123/merge
。 当且仅当 GitHub 端软件决定可以合并我们的 PR 时,才会创建merge
名称;在这种情况下,merge
引用指向 GitHub Git 已经进行的合并提交。head
ref 特指我们在单击"发出拉取请求"按钮时选择的分支尖端的提交。
如果我们将新提交推送到我们的 PR,则head
ref 会更新,merge
ref 会被销毁,并在可能的情况下创建一个新提交,使用与往常相同的规则。
公关后
在这一点上,无论谁控制了我们称之为upstream
的GitHub分叉,都有各种选择。 他们有一个拉取请求,该请求有一个数字。 他们可以检查拉取请求。 他们可以使用 GitHub 为 PR 创建的特殊名称git fetch
将其放入笔记本电脑上的Git 存储库中(请参阅脚注 5)。 或者他们可以只使用 Web 界面上的各种点击按钮。
如果他们确实使用这些点击按钮,GitHub 特别提供三个按钮,GitHub 以这种方式标记:
合并。 这做了一个直接的
git merge
,就像 Git 会这样做一样。6您的所有提交及其哈希 ID 现在都可以从它们合并到的任何分支访问。 其分支上存在一个新的合并提交;这个新的合并提交在您的 Git存储库中的任何位置都不存在。7挤压和合并。 这实际上运行
git merge --squash
,尽管不像 Git 那样,因为在命令行 Git 中,git merge --squash
实际上并没有提交任何东西。 在这种情况下,他们会在他们的分支上进行一个新的提交来合并你的工作,但他们不接受你的任何提交。变基和合并。 这实际上运行
git rebase --no-ff
,将所有提交复制到具有新的和不同的哈希ID的新提交。
最后,这给我们带来了你的问题:
如何从上游拉取合并,而无需从上游获取我不想要/不需要的其余分支
答案取决于您在两个存储库中的每一个中想要的内容:GitHub 分支和笔记本电脑存储库。
如果他们进行了真正的合并,您可以执行以下操作:
git fetch upstream
git checkout desired-branch
git merge --ff-only upstream/theirbranch
因为来自名为branch
的分支的提交现在位于其分支中。 您需要做的就是添加最终的合并提交。 您不再需要任何额外的名称来记住用于创建和发送拉取请求的分支提示,因此请随意删除这些名称。
如果他们做了一个壁球并合并或变基并合并,这个--ff-only
将失败。 现在由你决定:你想放弃你原来的提交,转而支持他们放入upstream/theirbranch
的任何提交吗? 无论您是否这样做,您现在都可以使用 Git 的所有工具。 他们的提交在upstream/theirbranch
:您可以使用git log
查看它们。 可以通过分支名称访问您的提交。 您可以使用git branch -f
或git reset --hard
放弃部分或全部提交。 您可以重命名分支以保留旧提交,同时确保其工作。 你可以做任何你喜欢的事情! 毕竟,您的存储库是您的。
6事实上,由于 GitHub 已经做到了——它在refs/pull/number/merge
——他们实际上不需要在这里做任何事情。 如果合并有冲突,则此引用不存在,并且"合并"选项被禁用。
7由于 GitHub 使用预制合并,因此从技术上讲,您可以获取该合并并将其保存在存储库中。 我不确定GitHub是否使用现有的合并 - 他们可以但不必 - 但可以通过实验来解决这个问题。 但请注意,GitHub 可以随时选择更改其工作方式,因此以一种或另一种方式依赖它可能是不明智的。
我认为将其他上游分支放在远程/修补程序/xyz 下没有缺点,所以我定期进行 git 获取。
要删除不再使用的本地分支,我使用 npm 包 git-removed-branch(此处推荐:https://stackoverflow.com/a/45699402/1974021)
更具体地回答这个问题:
如何在不获取其余部分的情况下从上游拉取合并
您可以执行git fetch origin master:master
,仅获取和合并这一个分支