完成 git 分支后的标准做法是什么?

What is the standard practice when you're done with a git branch?

我分叉了一个 repo,然后创建了一个名为 patch1 的分支进行了更改并提交了它们,然后我在上游创建了一个拉取请求并将其与上游合并到 master。但是在我的本地回购中,分支没有合并,我如何从上游拉合并而不从上游获取我不 want/need 的其余分支,例如 Hotfix12NewFeature5

这里的标准做法是什么?

我认为将其他上游分支置于 remote/hotfix/xyz 下没有任何缺点,所以我定期进行 git 提取。

要删除不再使用的本地分支,我使用npm包git-removed-branches (Recommended here: )

要更具体地回答这个问题:

how do I pull the merge from upstream without getting the rest

您可以git fetch origin master:master只获取并合并这一个分支

由于上游 PR 更改了 target 分支,您应该只需要 git pull 那个。

我实际上有一个脚本,它为本地存在的每个分支执行 git pull,但如果没有,我会这样做(假设您从 feature42 PR 到 master):

git checkout master
git pull

如果您的上游 PR 也删除了源代码分支,您可能也想在本地执行此操作:

git branch -d feature42

(首先,旁注:我认为 标准做法 不一定那么有趣,因为您的 Git 存储库是 您的。你可以做任何对你有用的事情! 所以标准的做法只有在如果对你有用。你可能得试试几种不同的方法。)

TL;DR

我推荐运行宁:

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。)

您现在可以在自己的笔记本电脑 Git 存储库和 GitHub 分支中自由删除为完成此 PR 而创建的任何或所有名称。这些名字几乎不用磁盘space,但是会用"head space"(精神能量)来记录,所以建议删除。

这个--ff-only合并在某些情况下会[=2​​85=]失败;在那些情况下,请参阅下面的长篇讨论。

记住这些关于Git的事情:

  • Git 就是关于 提交 。 Git 与 files 无关,甚至与 b运行ches 无关,它与 commits 无关。提交存储数据(文件快照)和元数据,例如创建者和创建时间。 所有 提交都是 100% read-only:任何提交的任何部分都不能更改。提交的真实名称是一个丑陋的大哈希 ID,该哈希 ID 对存储在提交中的 data-and-metadata 的每一位都非常敏感,因此几乎不可能更改提交的内容:如果您取出一个,稍微修改一下,然后放回去,你得到的是 新的和不同的 提交,具有新的和不同的哈希 ID。

  • B运行ch 名称masterdevelop 等等是有用的,因为它们让你 找到 提交。每个提交的真实名称是一个大而丑陋的哈希 ID,没有人能记住。但是我们没有记住一个又大又难看的散列ID,因为我们有一台电脑可以帮我们记住它,在一个名字下!

  • b运行ch这个词有歧义。每当有人谈论某些 Git b运行ch 时,请确保您知道他们的意思是 b运行ch name 还是其他。这与您的问题仅间接相关,但始终值得记住。另请参见 What exactly do we mean by "branch"? 一般来说,b运行ch 这个词是否表示 b运行ch name,或一些提交集合,以 a b运行ch名称指定的一个结束。也有人用它来表示remote-tracking name(我这里尽量不这么说)

  • 您的存储库是 您的。你有自己的 b运行ch 名字。您的 b运行ch 名称不是任何其他 Git 的 b运行ch 名称。你的 Git 和他们的 Git really 共享的是 commits,通过他们的哈希 ID。 (由于提交是 100% read-only,如果你的 Git 和他们的 Git 可以真正共享提交,那很好。如果不是,你的 Git 和他们的 Git 可以有单独的副本。副本 不能 更改,正如我们已经指出的那样。)

  • 除了b运行ch names,Git还有更多的方式——也就是更多的names——它可以记住任何一个特定提交的哈希 ID。其中一种名称是 remote-tracking 名称 ,如 origin/master。一个 remote-tracking 名字是 你的 Git 对 其他 Git 的 [=285] 的记忆=]b运行ch 名称(以及他们存储在该 b运行ch 名称中的哈希 ID)。

这最后两项是处理你的情况的关键。

I forked a repo ...

这意味着您使用某些托管服务提供商(例如 GitHub)在第一个 Git 存储库的基础上创建了第二个 Git 存储库。也就是说,在 GitHub 端,你做了一个 clone。您的 clone-on-GitHub 现在独立于他们的 clone-on-GitHub。

您可能在您自己的计算机(您的笔记本电脑或其他设备)上进行了克隆,因此可能存在 三个 克隆在此刻。没关系!在Git-world中,得到一个克隆,他们得到一个克隆,然后everyone gets a clone!有很多克隆没有问题……嗯,除了一个:每个克隆都有自己的 b运行ch 名称。可以管理 lot 个 运行ch 名称。

当您使用 "fork this repository" 可点击网页按钮时,GitHub 特别制作的克隆有一些特殊之处。事实上,有几件奇特的事情,但这里最重要的是s: this clone copies all b运行ch names from the repository you'重新分叉到您的 GitHub 克隆。您的 GitHub 克隆只有 b运行ch 名称,没有 remote-tracking 名称.

如果您随后 运行:

git clone <github-url>

为了将您的 fork 复制到笔记本电脑上的新克隆,第三个克隆 没有 复制所有 "their" b运行ch 名称。但是稍等一下:他们是谁?

  • 我们已经说过 两个 有趣的克隆在 GitHub 上。这里they的意思要看你用什么URL了。如果你使用了original仓库的URL,在你做fork之前,"they"是原始仓库。如果您使用叉子的 URL,则 "they" 就是您的叉子。

  • 如果你刚才 分叉了他们的存储库,你的 分叉具有完全相同的 b运行 ch 名称(和存储的哈希 ID 值)和所有相同的提交(带有它们唯一的哈希 ID)作为 他们的 分支。所以从某种意义上说,克隆哪一个并不重要。但随着时间的推移,你的分叉和他们的分叉可能会分开,因为你 and/or 他们向你的 and/or 他们的存储库添加更多提交。如果你和他们添加 不同的 提交,或者以不同的方式更新你和他们的 b运行ch names,那么它就开始变得重要了.

通常,此时您要做的是创建 两个 Git 调用 remotes 的克隆你的笔记本电脑。 remote 只是一个类似于 origin 的短名称,我们将让我们的(笔记本电脑)Git 存储 URL 以供其他 Git 存储库。当您 运行 git clone <url> 时,您的 Git 创建了这个标准的 origin 遥控器。由于在 GitHub 上有 两个 有趣的存储库——你的分叉和他们的分叉——你可能想要添加第二个遥控器,这样你每个分叉都有一个遥控器.第二个遥控器的标准名称是 upstream。 (这不是一个特别 的名字,因为 Git 中的其他几个东西在不同时期被称为 upstream,但它很常见,所以我们将在这里使用它。)

Remote-tracking 名字

让我们回到一个事实,即您的 laptop-side 克隆 没有 将任一分支的 b运行ch 名称复制到您的 laptop-clone的 b运行ch 名称,并查看为什么 GitHub "fork" 按钮 did 复制他们所有的分支 b运行 ch 名称到你的叉子。这一切都与 remote-tracking names.1 有关 你的笔记本电脑 Git 为每个人创建 remote-tracking 名字b运行ch 笔记本电脑 Git 在远程 Gits 中看到的名称。这些远程 Git 在您的笔记本电脑上有 名称 originupstream。所以你的笔记本电脑 Git 可以将这些名称贴在它们的 b运行ch 名称前面,然后将 GitHub Gits ' master——几乎肯定有两个——变成 origin/masterupstream/master。它将 GitHub Gits' develop 变成 origin/developupstream/develop。这对 每个 b运行ch name in each remote.

重复

保存所有这些额外名称的成本非常低:基本上不需要 磁盘space。那是因为 Git 完全是关于 提交 ,并且提交具有哈希 ID。假设 origin/master 表示 commit a1234567...,而 upstream/master 表示 commit a1234567...。你自己的 Git 已经有提交 a1234567...,所以你的 Git 必须存储一些 name-value 对: origin/master=a1234567..., upstream/master=a1234567....

关于 remote-tracking 名字的好处是:

  1. 他们根本不需要 space。 (Git一般存放在.git/packed-refs,是一个有记录的文件,而不是多个文件,所以往往占用的空间甚至不到一个磁盘块。你自己的b运行 ch 名称已经很便宜 storage-space-wise,因为其中大部分都存储在单个磁盘块中,但这些名称甚至更便宜。)

  2. 他们自动更新。当您 运行 git fetch origin 时,您的 Git 在 origin 调用 Git(您在 GitHub 上的分叉)。你的 Git 从他们的 Git 中获取任何新的提交和其他所需的对象,然后更新你所有的(笔记本电脑)origin/* remote-tracking 名称以匹配你的所有(GitHub-fork) b运行ch 名称。当您 运行 git fetch upstream 时,您的 Git 在 upstream 调用 Git(他们在 GitHub 上分叉)。你的 Git 从他们的 Git 中获取任何新的提交和其他所需的对象,然后更新你所有的 upstream/* remote-tracking 名称以匹配他们所有的 b运行ch名字.

您可能希望将 --prune 添加到 git fetch 命令,或在 Git 配置中将 fetch.prune 设置为 true,以便您的 Git 从您的 remote-tracking 名称中删除 任何 "their" 的 b运行ch 名称Git(你或他们在 GitHub 上的分支)不再有。如果没有 --prune,上面第 2 步中的更新永远不会注意到他们,无论他们是谁, 已删除 feature/tall,因此您的 origin/feature/tallupstream/feature/tall——不管它是什么——作为一个陈旧的 remote-tracking 名字流传。使用 --prunefetch.prune,您的笔记本电脑 Git 注意到该名称应该消失,并将其删除。

那么:为什么 GitHub "fork a repository" 按钮没有创建 remote-tracking 名称而不是 b运行ch 名称?好吧,只有 GitHub 才能真正回答这个问题;但如果他们有,你需要一些方法来操纵 remote-tracking Hub 上的名字。既然他们没有,他们只需要提供一种方法让你在 GitHub 上操纵 b运行ch 名称。请注意,GitHub 没有用于 fetch: 的可点击按钮,您无法创建 GitHub 分支 运行 git fetch!由于您在笔记本电脑上使用 git fetch 来更新 remote-tracking 名称,GitHub 上缺少 fetch 意味着您无法更新 remote-tracking 那里的名字。


1从历史上看,remote-tracking 名字实际上是在导致所有这一切的各种决定之后出现的,但我认为遵循另一个逻辑更有意义方式。


T运行正在提交:git fetchgit push

有两种常见的方法可以将提交提交到 Git 存储库中。我们已经在上面提到了其中之一,即 git fetch。你 运行 git 获取 <em> 远程 </em>,然后你的 Git 取出存储的 URL来自 remote-name——例如,origin 的 URL——并在该位置调用 Git。

Git 为您的 Git 列出了它所有的 b运行ch 名称(以及标签名称和其他内部名称,但这里我们只真正查看 b 运行通道名称)。每个 b运行ch 名称标识 one 提交,这是 b运行ch 的 tip。在那个 b运行ch 上可以访问的所有 earlier 提交,都可以使用那个 b运行ch 名称访问。有关 可达性 概念的详尽讨论,请参阅 Think Like (a) Git。了解可达性是使用 Git 的关键,因此如果您不熟悉这些概念,您一定要完成这些内容。

此时,您的 Git 可以向他们的 Git 询问您的 Git 想要或需要但没有的任何提交和其他内部 Git 对象.这一步实际上非常有趣,涉及到很多图论,但我们可以认为 g运行ted 两个 Git 做得很好。他们计算出了他们的 Git 拥有的、您的 Git 想要的一组相当小的 Git 对象。他们压缩这些对象——这就是这里所有 counting objectscompressing objects 消息的内容——并将它们发送过来。您的 Git 将这些放入您的集合中,将提交和其他内部对象添加到您笔记本电脑上的存储库中。这允许您的 Git 更新您的 remote-tracking 名称:您现在拥有他们拥有的所有提交,以及您没有给 他们的任何提交.

请注意,您的 remote-tracking 名称实际上是 他们的 Git 的 pre-reserved。您不调用任何您自己的 b运行ches origin/masterorigin/develop 之类的 2 所以 Git 可以自由粉碎并替换您的任何或所有 remote-tracking 名称:none of 您的 b运行ch 名称受到影响。

如果你想走另一条路,fetch的反面就是push。3但是这里有一个不对称。当你 运行 git push origin <em>b运行ch</em>, 你有你的 Git通过查找遥控器的 URL 再次调用其他 Git。但是这一次,不是让他们列出他们的 b运行ch 名称等等,而是将提交 带到 你的 Git,你的 Git 发送 他们提交和其他内部 Git 对象。您将 发送给 他们任何他们需要的提交,以使您自己的 b运行ch branch 有用——这包括您拥有的任何可到达的提交,他们没有 - 我们再次获得所有计数和压缩对象消息。但是现在,在将任何必需的提交 发送到 他们的 Git 之后,你的 Git 要求——通常是礼貌地——他们应该设置他们的 b运行ch name branch 到提交的哈希 ID,这也是你的 b运行ch branch.

他们没有设置 remote-tracking 名字! (特别是 GitHub 甚至 没有 remote-tracking 名称。)他们没有设置其他 reserved-space 名称。他们设置了他们的 b运行ch 名称。

当您的Git提出礼貌请求时,如果他们不喜欢,他们会拒绝请求。如果您正在创建一个新的 b运行ch 名称,他们通常会喜欢。但是,如果您要更新现有的,如果 new 哈希 ID 指的是 co,他们将不会喜欢更新previous 同名散列 ID 不可访问的 mit。

也就是说,考虑一些提交链:

...--G--H   <-- branch

现在我们将在末尾添加一些新的提交:

...--G--H   <-- branch
         \
          I--J

并提议他们将名字 branchH 移至 J。如果他们这样做,提交 H 仍然可以访问:从 J 开始并向后工作,我们从 JI 然后到 H。所以这个请求会被接受。但如果我们改为这样做:

...--G--H   <-- branch
      \
       K--L

并要求他们将他们的名字 branch 设置为指向 L,他们会拒绝,因为无法从 L 到达 H。来自 L 的可达提交是 L,然后是 K,然后是 G,然后是 G.

之前的其他提交

Git的术语是对名称branch的更改必须是fast-forward 。将 branchH 移动到 J 是 fast-forward;将 branchH 移动到 L 是非 fast-forward。4


2从技术上讲,您可以。 Git 有内部名称space,因此 Git 可以保持一切正常。但这不是一个好主意:您可能没有这些内部名称space, 会搞砸的。

3不是推拉,是推拉!这东西一个历史事故,我认为它会导致很多混乱,但事实就是如此。

4到force-push非fast-forward更新,可以使用--force标志,或者添加+ 标志到 refspec,这是我们在这里没有定义的东西。这两者都将礼貌的请求变成了命令。他们可以仍然拒绝,但我们不会在这里担心这些细节。


拉取请求

拉取请求 (PR) 本身就是一个 host-provider-specific 功能。 Git 没有拉取请求! (Git 有一个 git request-pull 命令,但它的作用是生成一封电子邮件。)请注意,如果我们拥有一个 GitHub 分支,我们可以 git push 到它。没关系:我们可以更新 我们的 分支。如果它们是 fast-forward,我们的 git push 操作将成功,在特殊情况下,我们可以 git push --force 使我们的操作成功,即使它们不是 fast-forward。所以我们可以 git push 一切我们想要的,到 我们的 Git Hub fork,我们称之为 origin。这让我们可以随心所欲地改变 GitHub 叉子的形状。我们的分支将像任何 Git 存储库一样存储 提交 。它会将它们存储在 b运行ch names 下,就像任何 Git 存储库一样。它没有 remote-tracking 名称——那些特定于我们的笔记本电脑 Git 存储库——但这没关系:我们不需要我们的叉子有 remote-tracking 名称。

但我们可能希望将我们的提交放入不是我们的Git集线器分支,在URL我们存储在名称upstream。我们将如何做到这一点?

If——这是一个很大的问题,如果这通常不是真的——另一个 GitHub 分支的所有者要给我们对他们的存储库的写入权限,我们可以 git push 我们直接提交到 upstream。但他们必须真的信任我们的存储库。

GitHub 可以提供某种特殊的 name-space: semi-protected b运行ch 名称模式,upstream 分支的所有者可以提供给我们写上,他们不会使用自己。 GitHub 可以有一个执行机制来完成所有这些工作。但他们没有。相反,GitHub 给我们 拉取请求

在我们进行 PR 之前,我们首先 git push-ing 我们的 laptop-made 提交到我们自己的 GitHub 分叉 origin。这些提交通过它们的哈希 ID 进入,根据我们的 git push 命令更新 b运行ch 名称 in 我们的 GitHub 分支。最终,在我们的 GitHub 分支中,一些 b运行ch 名称指向我们喜欢的一些提示提交,我们想提供给操作 GitHub 的人我们称之为 upstream.

正是在这一点上,我们提出了拉取请求。我们使用 GitHub 的接口将提交从我们的 GitHub 分叉发送到他们的 GitHub 分叉(就像通过 git push),但它们显示在分叉中的 upstream 在 GitHub 人员控制的特殊名称下。5/sup> 除了单击 "make a PR" 按钮外,我们在此过程中没有任何代理:GitHub确定特殊名称,并为 PR 创建名称。 GitHub 然后还会发送电子邮件、松弛消息等——任何可能合适的——来提醒那些 运行 我们称之为 upstream 的分叉的人有一个新的拉取请求给他们.

现在一切都取决于他们


5这些是refs/pull/*名字space。这个名字space中的东西是有编号的:每个 PR 或问题在 GitHub 存储库中都有一个唯一的计数,当我们创建一个新的 PR 时,GitHub 给它一个麻木的编号r——为了具体起见,我们假设 123——并创建 refs/pull/123/head 形式的名称,也可能是 refs/pull/123/merge,也可能不是。当且仅当 GitHub 端软件决定我们的 PR 可以合并时,才会创建 merge 名称;在这种情况下,merge ref 指向 GitHub Git 已经进行的合并提交。 head ref 特指我们在单击 "make pull request" 按钮时选择的 b运行ch 顶端的提交。

如果我们将新提交推送到我们的 PR,head ref 会更新,merge ref 会被销毁,如果可能的话会创建一个新的 ref,使用与往常相同的规则。


PR 后

在这一点上,无论谁控制了我们称为 upstream 的 GitHub 分支,都有各种选择。他们有一个拉取请求,其中有一个数字。他们可以检查拉取请求。他们可以使用 git fetch 和 GitHub 为 PR 创建的特殊名称,将其放入 他们的 笔记本电脑上的 Git 存储库(参见脚注5).或者他们可以只使用网络界面上的各种点击按钮。

如果他们使用那些点击按钮,GitHub 特别提供三个按钮,GitHub 标签是这样的:

  • 合并。这直接 git merge,与 Git 相同。6 所有 你的 提交,以及他们的哈希 ID,现在可以从它们合并到的任何 b运行ch reachable。他们的 b运行ch 上存在一个 new 合并提交; 您的 Git 存储库中的任何地方都不存在这个新的合并提交。7

  • 压缩并合并。这实际上是 运行s git merge --squash,尽管不完全像 Git 那样,因为在命令行 Git 中,git merge --squash 实际上并没有提交任何东西。在这种情况下,他们在他们的 b运行ch 上做了 one 新提交,合并了你的工作,但他们不接受任何 your 提交。

  • 变基并合并。这实际上 运行s git rebase --no-ff复制 你所有的提交到新的和不同的哈希 ID 的新提交。

最后,我们要回答您的问题:

how do I pull the merge from upstream without getting the rest of the branches from upstream that I don't want/need

这个问题的答案取决于您希望在您的两个存储库中的每一个:您的 GitHub 分支和您的笔记本电脑存储库。

如果他们进行了真正的合并,你可以这样做:

git fetch upstream
git checkout desired-branch
git merge --ff-only upstream/theirbranch

因为 你的 来自你的名为 branch 的 b运行ch 的提交现在在 他们的 b运行通道。您需要做的就是添加最终的合并提交。您不再需要任何额外的名称来记住您用于创建和发送拉取请求的 b运行ch 提示,因此请随时删除它们。

如果他们压缩并合并变基并合并,这个--ff-only失败。现在由您决定:您是否要放弃原始提交,转而支持 他们 放入 他们 [=135] 的任何提交=]?无论您是否这样做,您现在都可以使用 Git 的所有工具。他们的提交在 upstream/theirbranch 上:您可以使用 git log 查看它们。您的提交可以通过您的 b运行ch 名称访问。您可以使用 git branch -fgit reset --hard 放弃部分或全部提交。您可以重命名您的 b运行ches 以保留您的旧提交,同时确保它们的工作。你可以为所欲为!毕竟,您的存储库是 您的


6事实上,由于 GitHub 已经做到了——它在 refs/pull/<em>number</em>/merge——他们实际上不需要在这里做任何事情。如果合并有冲突,则此引用不存在并且 "merge" 选项被禁用。

7因为 GitHub 使用 pre-made 合并,从技术上讲,您 可以 获取该合并并将其放入您的存储库.我不确定 GitHub 是否使用现有的合并——他们可以但不必——但可以通过实验来解决这个问题。不过请注意,GitHub 可以随时选择更改它的工作方式,因此以这种或那种方式依赖它可能是不明智的。