是否有可能在不污染先前的拉取请求的情况下提交一个分支?如果没有,如何将主分支变成分支?

is it possible to commit to a fork without polluting a previous pull request? If not, how do I make the main into a branch?

Ref: 大约 9 年前的以下问题:
Pull request without forking?

背景:
我正在研究 GitHub/Git,并且正在 运行 解决问题。我进行了不懈的搜索,但没有找到任何解决此特定问题的方法 - 我发现的最接近的是上面提到的问题。

问题:
我“分叉”了一个存储库,打算做一些工作,对我自己的分叉进行更改,然后创建一个拉取请求返回到原始项目,作为对其做出贡献的一种方式。

我终于想通了,并且能够成功创建包含我提议的更改的拉取请求。

请注意,我还想做其他事情来为这个项目做出贡献,在创建拉取请求后,我继续工作并向我的本地副本做出额外的提交,包括导入一些技术文档等。

显然,无论出于何种未知原因,在我发出拉取请求后,拉取请求“拥有”我对原始回购的分叉,此后我所做的任何事情都成为那个 pull request - 它是否相关并不重要,我是否将它推送到项目的分支,我是否将它添加到 PR,或者其他什么。它看起来就像变魔术一样,只有在我 remove/revert 我自己的存储库 fork 中的更改才能被删除。

这是否意味着与该项目有关的所有工作都必须完全停止,直到该 PR 被接受 and/or 拒绝?如果是这样,其他人,尤其是在单一代码库上工作的公司,如何设法完成工作?

当然,我相信这是可能的,人们总是这样做。

我所做的研究没有披露任何似乎可以解决这个特定问题的内容,但是对不同问题的其他答案似乎暗示了这样一个事实,即一旦您分叉回购并创建拉取请求,拉取请求 似乎“拥有”您本地存储库的那个实例 - 缓解这种情况的唯一方法是:

要完成额外的工作,无论在项目的哪个位置,您都必须:

“冲洗并重复”任何您想做的额外工作,最终得到一个分支比圣诞树还多的叉子。

这引发了几个问题:

  1. 这是真的吗?我理解正确吗?
  2. 为什么?这似乎不必要地复杂和令人费解,尤其是对于单个贡献者。

最后也是最重要的问题:

3。如何清理我的本地副本?显然我应该克隆 repo,然后创建一个分支来工作,然后创建 pull request。 (有没有办法把我更新的“主”,变成一个分支,然后重新创建原来的 main 这样我就可以创建额外的分支来做额外的工作?)

我犹豫是否只是“破解”现有的 repo 试图解决问题,因为我不想污染原始的 pull request 或搞砸上游项目。

谢谢!

当您执行拉取请求时,您建议将您的一个分支合并到原始存储库的一个分支中。每次更新分支时,合并都会更新。 这在您修复或审查后更新时非常有用。

针对您的案例的几种解决方案,简单的关闭您的拉取请求,为您要提交的每个主题创建一个分支(每个分支基于分叉存储库的主干)。

第二种方案: 创建一个分支来让你做额外的工作 回到主要分支(或主人) 强制已经提交的分支到原始提交 并推送它

git checkout -b my_second_feature
git checkout main
git reset --hard <commit_sha>
git push -f

注意:这很长,但您确实需要了解这些内容。我已经 运行 超出了 space(字符限制为 30k)所以我将把它分成两个单独的答案。 Part 2 is here; part 3 here.

虽然“拉取请求”不是 Git 的一部分(它们特定于 GitHub 1),即使没有特别提到 GitHub,我们也可以说一些关于它们的事情。然后我们可以稍后插入 GitHub-specific 项目。那么让我们从这个开始:

  • Git 就是关于 提交 。虽然 Git 提交包含文件,但 Git 并不是真正关于文件,而是关于提交。而且,虽然我们使用分支名称来 find 提交,但 Git 也不是关于分支名称:它实际上只是关于提交。

  • 这意味着您需要了解有关提交的所有信息:什么是提交、每次提交以及连续的一串提交可以为您做什么。

所以我们将从一个提交的快速概览开始,然后查看一串连续的提交。


1Bitbucket 也有“拉取请求”,但它们略有不同,GitLab 有“合并请求”,这又是 same-but-different。所有这些都建立在 Git 适当的相同基础支持上。


提交

每个 Git 提交都有编号。但是,这些数字不是简单的连续计数数字:我们没有提交#1 后跟#2 和#3 等等。相反,每个提交都会获得一个唯一的 哈希 ID——在 所有存储库中都是唯一的 ,即使它们与您的存储库根本无关2似乎是随机的,但实际上不是。3 散列 ID 又大又丑又不可能供人类使用:计算机可以处理它们,但我们脆弱的大脑会变得混乱。因此,在下面,我将使用伪造的哈希 ID,我只使用一个大写字母来代替真实的哈希 ID。请注意,要使这些哈希 ID 起作用,提交的每个部分 都必须 完全 read-only。也就是说,一旦您进行了新的提交,该提交就会永远冻结。那个特定的哈希 ID,无论它得到什么哈希 ID,都是针对 提交的,任何其他提交——过去、现在或未来——都不能使用该哈希 ID。

无论如何,每个 Git 提交都会存储两件事:

  1. 提交存储每个文件的完整快照(无论如何,Git 在您或任何人创建它时都知道).为了防止存储库变得非常胖,这些文件被 (a) 压缩和 (b) de-duplicated。因此,它们以只有 Git 可以读取的格式存储,没有任何内容,甚至 Git 本身也不能覆盖。正如我们将看到的,这解决了一些问题,但也带来了一个大问题。

  2. 提交还存储一些元数据,或有关提交本身的信息。这包括,例如,进行提交的人的姓名和电子邮件地址(来自他们的 user.nameuser.email 设置,他们可以随时更改,因此未经验证是不可靠的,但它仍然有用)。它包括一条 日志消息: 当您为自己的提交提供一条消息时,您应该写下对 为什么 您进行提交的解释。 您所做的——例如将 7 的一个实例更改为 14——是 Git 可以自行显示的内容,但是 为什么你把7换成14?是从几周到两周,还是因为 7 个小矮人都被克隆了?

在提交的元数据中,Git 为自己的目的添加了 previous 提交的原始哈希 ID 列表。这个列表通常只有一个元素长:对于 merge 提交(我们不会在这里介绍)它是 two 个元素长,并且至少任何 non-empty 存储库中的一次提交是 第一个 提交,其中没有任何先前的提交,因此此列表为空。


2这就是哈希ID必须如此大且丑陋的原因。严格来说,它们在两个永远不会 相遇 的存储库中不必是唯一的,但是 Git 不知道两个存储库是否或何时会相遇将来,如果两个 不同的 提交当时具有 相同的哈希 ID,就会发生不好的事情。我将这样的提交称为 Doppelgänger,一种预示着灾难的邪恶双胞胎。真正的灾难是——或者至少 应该是 ——只是这两个 Git 存储库的会议失败了。在一些非常旧的 Git 版本中,由于错误,确实发生了更糟糕的事情。在任何情况下,它根本不应该发生,哈希的大小有助于避免这种情况。

3当前哈希是提交中所有数据的 SHA-1 校验和,其中包括有关导致提交的提交的数据,因此它是导致该点的整个历史的校验和。 SHA-1 is no longer cryptographically secure. Though this does not break Git by itself, Git is moving to SHA-256.


提交链

鉴于上述情况,我们可以在一个很小的 ​​three-commit 存储库中绘制三个提交,如下所示:

A <-B <-C

提交 C 是我们的第三次也是 latest-so-far 提交。它有一些 random-looking 哈希 ID,以及所有文件的快照。一个或两个文件 in C 可能与早期提交 B 中的所有文件不同,其余的与 B 中的相同因此从字面上与早期提交 B 共享。所以他们不接受任何实际的 space。修改后的文件确实需要一些 space,但它们是压缩的——有时压缩得非常厉害——可能几乎不需要任何 space。提交元数据有一点 space(顺便说一句,它也被压缩),但总的来说,这个 full-snapshot-of-every-file 可能不会占用太多 space.

同时,提交 C 包含早期提交 B 的原始哈希 ID。我们说C指向B。这意味着如果 Git 可以 找到 C——我们稍后会看到它是如何做到的——Git 可以使用哈希 ID C中也能找到B。 Git 然后可以从 both 提交中提取两个快照中的所有文件,并比较它们。比较文件的结果是 diff: 说明,用于将 B 中的文件更改为 C 中的文件(反之亦然,如果你有差异按其他顺序完成)。

Git,以及像 GitHub 这样的网站,通常会 显示 提交作为差异,因为这通常比显示原始快照更有用.但是,如果您愿意,您可以轻松地获取快照:对于 Git 来说,有时 比获取差异 更容易 。 (由于 de-duplication 技巧,git diff 可以快速跳过相同的文件,但它仍然必须查看两个提交,而不仅仅是一个。所以对于哪个更容易有点混杂.)

提交 B,作为提交,具有快照和元数据,并向后指向 still-earlier 提交 A。但是提交 A 第一次 提交,因此它的元数据 不会 列出任何更早的提交。这意味着根据定义,其快照中的所有文件都是新文件。 (它们会被压缩并 de-duplicated 针对任何其他提交中的任何文件,但当时,这是 第一次 提交,所以它们只被压缩并且 de-duplicated against themselves。最后这意味着如果第一个提交包含一个大文件的 100 个相同的副本,那么在提交 A.)

分支名称和其他名称

Git 需要一种快速的方法来找到某个链中的 last 提交。 Git 可能会迫使我们——使用 Git 的人——记下最后一次提交的哈希 ID,在本例中为 C。我们可以将其保存在纸上、白板或其他东西上。但这很愚蠢:我们有一台 计算机 。为什么不让计算机将这些哈希 ID 保存在文件或其他东西中?事实上,为什么不让 Git 我们保存最近的哈希 ID

这正是分支名称的含义:一个保存 最新 提交的哈希 ID 的地方。 Git只需要最新的,因为最新的指向回second-latest,后者指向回一个still-earlier,以此类推。这会尽可能长时间地进行,仅当 没有更早的提交时才结束,这就是 Git 的工作方式:它从我们告诉它的提交开始——通常是通过分支名称—并且 向后.

让我们绘制一个简单的提交链,以散列 ID H 结尾(对于散列),并让分支名称 main 指向(包含散列 ID)H :

...--G--H   <-- main

现在让我们添加一个新名称,例如feature1。这个名字必须指向一些现有的提交。我们可以选择 G,或 H,或一些更早的提交,但选择 H 似乎很自然,因为它是我们最新的:

...--G--H   <-- feature, main

注意 Git 有很多种名称——不仅仅是分支名称——它们都做这种事情,即指向一个提交。所以我们可以创建一个 tag 指向提交 H,例如:

...--G--H   <-- feature, main, tag: v1.0

不过,大多数情况下,我们将只使用分支名称,这就是我现在在这里显示的全部内容。

在分支机构工作

Git 有它自己的特殊功能让我们做 工作 。正如我们之前提到的,提交快照的内容会一直冻结,并且只能由 Git 本身读取。所以我们实际上不能处理/使用提交中包含的这些文件。我们必须 Git 才能 提取 某处的文件。那个“某处”是我们的 working trework-tree.

Git还有一个很重要的东西,其中Git给出了三个名字:indexstaging area,有时 缓存 。我们不会在这里讨论这一点,除了要注意当你 运行 git commit, Git 实际上 使 来自文件的新提交 Git 的索引 / the-staging-area,而不是来自工作树中的文件。 所有 要提交的文件必须在暂存区中:这些是Git 知道的文件。提取提交会将提交的文件 复制到 暂存区以及工作树,以便它们从那里开始。

无论如何,一旦文件在您的工作树中,它们就只是您计算机上的普通文件。他们不再是 Git。它们来自 out of Git(来自提交),您可以将它们放回 into Git new commit later,但是当你做你的工作时,你在处理和使用 不在 Git 中的文件。只有 提交的 文件在 Git.

您像往常一样使用 working-tree 文件和 运行 git add 进行工作。 (这会将您列出的文件的工作树版本复制回索引,以便它们可以提交。在 git add 阶段,Git 进行初始压缩,de-duplication。在 Git 的索引中看到的文件是 de-duplicated 之前的文件,换句话说,这意味着索引的副本大多不带 space,除了您拥有的任何文件changed-and-added。您可以添加一个 未更改的 文件:这只是一点点浪费时间,因为 Git 会发现它是一个副本并只保留原始文件。它是浪费便宜的 计算机 时间,而不是宝贵的 人类 时间,所以随意浪费吧!但是如果你知道某些文件很大并且这也会浪费的时间,请跳过。)

无论如何,现在您的新提交已准备就绪,您 运行 git commit。这个:

  • 收集任何必要的元数据,例如您的姓名和电子邮件地址以及当前日期和时间;
  • 获取当前提交的散列 ID——您之前检查过的用于填充工作树(和 Git 的索引)的哈希 ID;
  • 冻结索引的快照;和
  • 将所有这些作为新提交写出,它获得一个新的、唯一的哈希 ID。

如果你有:

...--G--H   <-- feature, main

就在刚才,您的 当前 提交是 H,因此您的新提交(我们称之为 I)指向回H:

          I
         /
...--G--H
然而,

Git 确实需要知道 哪个分支名称 find H .所以这两个名字之一有一个特殊的名字 HEAD “附加到它”。假设这个名字 过去是,现在仍然是 feature。那么我们的绘图现在看起来是这样的:

          I   <-- feature (HEAD)
         /
...--G--H   <-- main

也就是Git用HEAD找名字feature,先找哈希IDH,现在写新的哈希ID[=72] =] 进入 feature.

这样做的效果是当前分支名称,无论它是什么,现在都指向您刚刚进行的新提交。 (请注意,I 中的快照使用了索引 / staging-area,您更新它以匹配您的工作树,因此所有三个现在都匹配,就像它们在您开始“干净”结帐或git switch.) 如果您使用通常的 modify-files-add-and-commit 过程进行另一个新提交,您将得到:

          I--J   <-- feature (HEAD)
         /
...--G--H   <-- main

如果你现在git switch maingit checkout main,Git所做的是:

  • 删除所有commit-J文件并替换为commit-H文件;和
  • 将特殊名称 HEAD 附加到 main

您现在拥有:

          I--J   <-- feature
         /
...--G--H   <-- main (HEAD)

你是 on branch main,正如 git status 所说,你的工作树和临时区域是“干净的”(匹配 H 提交),你的更新文件已安全保存永远——或者只要提交本身持续——在提交 J 中,您可以使用名称 feature.

找到它

如果愿意,您现在可以创建一个新分支,例如 feature2,然后切换到它(使用 git branchgit switch,或者组合 git switch -c一次完成):

          I--J   <-- feature
         /
...--G--H   <-- feature2 (HEAD), main

当您在此新分支上进行新提交时,分支名称会自动更新以指向最新提交:

          I--J   <-- feature
         /
...--G--H   <-- main
         \
          K--L   <-- feature2 (HEAD)

请注意,通过 H 并包括 H 的提交,在 Git 的术语中,在 所有三个分支 上。提交 I-J 当前 feature,提交 K-L feature2 .提交 H 是在 main 上的 最新 提交,尽管它不是有史以来的最新提交(即 您的[中的提交 L =595=] 存储库,此时)。此外,提交 JL 之间没有直接关系:它们只是可以说是表亲。他们是children的parent,parent,H的children。

正在合并

要了解会发生什么,我们现在需要查看通常的 harder-case 合并。 Git 有一个简单案例的快捷方式,但出于各种原因(有些好,有些不太好),GitHub 尤其从不使用此快捷方式。一旦你理解了更一般的情况,简单的情况就更容易理解了。

在Git中,使用git merge是关于组合工作。在namemain中不画就画两个特征分支(可能还存在,只是挡住了我想画的)。我们先切换到分支feature

          I--J   <-- feature (HEAD)
         /
...--G--H
         \
          K--L   <-- feature2

我们当前的提交现在是 J,我们现在会在我们的工作树中找到 J 的文件。我们现在 运行 git merge feature2git merge:

  • 找到提交 J(简单:只需阅读 HEAD 然后 feature);
  • 找到提交 L(也很简单:feature2 包含正确的哈希 ID);
  • 找到最佳共同起点提交。

最后一部分可能很难,尽管在这里很容易看出这是提交 HJL 的祖父。如果 Git 现在将 H 中的 快照与 J 中的 快照进行比较,Git 将生成一个食谱,其中包含您在 feature:

上所做的所有工作
git diff --find-renames <hash-of-H> <hash-of-J>   # what "we" did

通过 运行从 HLsecond diff,Git 将生成包含所有内容的配方在 feature2:

上完成的工作
git diff --find-renames <hash-of-H> <hash-of-J>   # what "they" did

在这一点上,谁做了哪些工作并不重要:唯一重要的是“我们”更改了哪些文件,“他们”更改了哪些文件,以及我们对每个文件做了哪些更改.两个 git diff 解决了这个问题。

如果Git可以单独组合这两组变化,它可以应用组合的变化到来自 H 快照。不管你喜欢怎么看,这要么保留我们的更改并添加他们的更改,要么将两个更改加在一起,或者其他什么。 Git 假定最终结果是 存储在新提交中的正确快照

如果 Git 不能 单独组合这些更改,Git 将在合并中间停止,并带有 合并冲突。程序员现在必须得出正确的结果。我们将跳过这部分。我们假设 Git 自己得出了正确的结果。在这种情况下,git merge 会为您继续 运行 git commit

通常,生成的提交 M 会将提交 J 作为其 parent。我们新的 merge commit does 实际上有 J 作为 a parent — 第一个 parent— 但也有提交 L,我们在 git merge 命令行上命名的提交,作为它的 第二个 parent,像这样:

          I--J
         /    \
...--G--H      M   <-- feature (HEAD)
         \    /
          K--L   <-- feature2

name feature 附加到 HEAD,照常移动指向新提交 M。但是由于 M 向后指向 J L,提交 K-L 是现在“在”分支feature。这意味着通过 M 的所有提交都在 feature,而 feature2 仍然在 L 结束并且 包含提交 I-J.

如果需要,我们现在可以删除名称feature2:只有直接找到L才有用,如果我们不这样做觉得需要直接找L,有需要的时候看M的第二个parent就可以找到。如果我们现在想向 feature2 添加更多提交,我们应该保留名称并执行此操作:

          I--J
         /    \
...--G--H      M   <-- feature
         \    /
          K--L--N--O   <-- feature2 (HEAD)

如果愿意,我们现在可以再次将 feature2 合并到 feature 中:

          I--J
         /    \
...--G--H      M-----P   <-- feature (HEAD)
         \    /     /
          K--L--N--O   <-- feature2

制作一种鸭头图片,但我们也可以重画它而没有沿着顶行的肿块:

...--G--H--I--J--M-----P   <-- feature (HEAD)
         \      /     /
          K----L--N--O   <-- feature2

(不确定这个长什么样子)。

Fast-forwarding

特殊的 short-cut 案例 Git 对 git merge 适用于这种情况:

...--D--E   <-- main (HEAD)
         \
          F--G   <-- bugfix

如果我们 运行 git merge bugfix,Git 将找到提交 EG,然后找到 merge baseEG:两个分支上的最佳提交。但那是提交 E 本身,即 当前提交 .

Git 可以 继续 diff E 与自身比较,发现没有变化。然后它 可以 EG 进行比较以找到它们的变化。然后它将应用这些更改 E 并提出一个新的提交 H,并给它两个 parents:

...--D--E------H   <-- main (HEAD)
         \    /
          F--G   <-- bugfix

提交 H 将是一个 merge 提交,有两个 parent,就像“真正的合并”情况一样。但显然将 E 与自身进行比较是愚蠢的,添加他们的更改只会让我们得到一个 cmmit H 其快照 完全匹配 其提交 G 中的快照。所以 Git 在这种情况下,除非我们告诉它,否则根本不会费心合并。

相反,Git 将执行它所谓的 fast-forward 合并 。这意味着 Git 直接检查提交 G,同时向前拖动当前分支名称:

...--D--E
         \
          F--G   <-- bugfix, main (HEAD)

现在根本没有理由在图中画出扭结:

...--D--E--F--G   <-- bugfix, main (HEAD)

和删除名字bugfix显然是足够安全的,虽然据推测main会在后面更进一步。

抑制 fast-forward 而不是合并,我们会运行 git merge --no-ff。 GitHub 实际上总是这样做,因此您不会看到 fast-forward 合并发生 on GitHub;但很高兴了解他们。

何时删除名称

何时以及是否删除其他分支名称由用户决定。请注意,删除 name 不会删除 commits: 它只会让它们更难找到。但还有一件事要知道。假设我们有:

...--G--H   <-- main
         \
          I--J   <-- bugfix (HEAD)

其中提交 IJ 根本不起作用。你会 运行:

git switch main
git branch -d --force bugfix

放弃您修复错误的尝试。这给你留下了:

...--G--H   <-- main
         \
          I--J   ???

提交 I-J 仍然存在,但除非你写下 J 的哈希 ID,否则你可能永远无法 find 再次提交 J

Git 最终会检测到提交 Junreachable(您无法找到它)并将删除它真实的。一旦 J 消失,提交 I 也是如此。你有一个宽限期,通常至少 30 天,在此期间 Git 不会 执行此操作,以及各种 Git 命令来帮助查找 accidentally-lost 承诺。但是,如果您不费心找到它们并重新添加名称,Git 用来跟踪“丢失”提交的“reflog 条目”最终会过期,然后——当 Git 绕过时进行维护和清洁工作——“丢失”的提交将真正从这个存储库中消失。因此,虽然提交是 read-only,但它们只是“大部分是永久性的”。只要您能找到它们,它们就会保留在您的存储库中(然后会更长一点)。

克隆、远程和多个存储库

Git 不仅仅是一个版本控制系统 (VCS);这是一个 分布式 VCS (DVCS)。 Git 进行此分发的方式是允许——或者更确切地说,强烈鼓励——存储库的许多副本存在。因此,一个 Git 存储库 是:

  • a collection 提交和其他 Git objects,其中一些或全部也可能在其他存储库中;和
  • 一个 collection 名称,例如分支和标签名称,可以帮助您(和 Git)找到 提交和其他内部 objects.

这些存储为 refs/ 下的两个简单 key-value databases. The keys in the names database are branch names like refs/heads/main, tag names like refs/tags/v1.2, and many other kinds of names. Each name lives in a namespace。每个名称只存储一个哈希 ID。

objects 数据库中的键是散列 ID。此数据库中的每个 object 都有一些 Git 内部 object 类型(提交、树、blob 或带注释的标记)。提交 objects,连同支持树和 blob objects,结束存储你的文件;并且您将主要处理提交,通常不必太在意这些细节。

由于提交哈希 ID 全局 唯一,您的 某些存储库的克隆中的 object 数据库密钥是 与同一存储库的每个其他克隆中的密钥相同。当您克隆存储库时,您将获得所有或几乎所有的 提交 和支持 object。但是 names 数据库在 你的克隆 中与他们的完全分开。

这意味着存储库的克隆开始时根本没有分支名称。你运行:

git clone <url>

或:

git clone -b <branch> <url>

并且您的 Git 软件会创建一个新的 totally-empty Git 存储库以启动。您的 Git 软件使用您的 Git 存储库(我喜欢将其缩短为“您的 Git”)调用 他们的 Git软件并将其指向 他们的 Git 存储库(“他们的 Git”)。他们的 Git 列出了他们所有的分支和标签以及其他名称和哈希 ID,然后你的 Git 要求它想要复制的 objects(通常,他们全部)。对于您将要获得的每个提交,他们的 Git 有义务提供该提交的所有 parent,以及 parent 的 parent,等等.所以你最终将每个提交复制到你的 Git.

现在您拥有所有 提交 (并支持 object),您的 Git 需要他们的每一个 分支名称 重命名 它们。这个重命名过程利用了“远程”的概念。

一个remote,在Git中,只是一个短名称,至少存储了一个URL(以后你可以让它存储各种额外的功能). URL 是您在 git clone 中键入的那个,第一个“远程”的 名称 始终是 origin. 4 所以 origin 从现在开始意味着 我从 克隆的 URL,除非你改变了一些东西。

Git 使用这个名称——origin 字符串——为 their 分支组成 new names名字。他们的main变成了你的origin/main;他们的 debug 变成了你的 origin/debug;如果他们有 feature/tall,你会得到 origin/feature/tall;等等。这些名称实际上并不是 branch 名称;我喜欢称它们为 remote-tracking names.5 它们的功能是记住,对于您的 Git 存储库,什么 他们的 分支名称,什么提交 每个选择的名称,你的 Git 上次从他们的 [=824] 获得更新=].

重命名完成后,您的 Git 已经为他们拥有的每个分支名称创建了 remote-tracking 名称。您拥有他们所有的提交,并且可以找到所有这些,因为您的 remote-tracking 名称拥有与其分支名称相同的哈希 ID,他们正在使用它们来查找他们的提交。

现在,在您的 git clone 完成并 returns 控制您开始工作之前不久,您的 Git:

  • 根据您提供的 -b 参数在您的存储库中创建一个新的 分支名称 :如果您说 -b bugfix,您的 Git找到与他们的 bugfix 相对应的 origin/bugfix 并创建 你自己的 bugfix,指向相同的提交。
  • 签出(切换到)这个新分支。

所以现在你的克隆中有 一个 分支,匹配他们的一个分支。如果您不使用 -b,您的 Git 会询问他们的 Git 他们推荐什么名字。通常的标准推荐是他们的主要分支(现在通常是main;过去是master)。

获得克隆后,您可以使用 git remote add 添加 更多 个遥控器。这需要一个名称 for 遥控器,以及一个 URL;它设置了遥控器,但还没有 运行 git fetch。现在是时候讨论获取和推送了;查看其他答案。


4您可以选择其他名称,但这样做几乎没有任何意义。使用 origin 作为“主遥控器”的名称。您可以随时重命名遥控器,因此即使您不打算保留开头的 URL,让 git clone 默认为 origin 也可以正常工作。

5Git 调用它们 remote-tracking 分支名称,击败可怜的重载词 branch 从血腥、畸形的野兽到 barely-recognizable-splotch。说真的,把branch这个词放在这里,没有任何帮助。

第 2 部分—参见

git fetch

到 运行 git fetch,你选择一个遥控器并调用它作为 git fetch <em>remote</em>.如果您省略远程名称,Git 会从某个地方选择一个远程,或者尝试使用默认名称 origin,具体取决于很多配置项。如果你只有 一个名为 origin 的单一标准遥控器,运行ning git fetch 没有额外的参数是好的:你没有别的意思无论如何。

fetch 的作用是:

  • 调用任何 Git 软件回答存储的 URL;
  • 让他们列出所有名称(分支、标签等)和相应的哈希 ID;和
  • 从他们那里获取他们拥有而您没有的任何提交。

请注意,这与我们对 git clone 执行的操作相同,只是现在不是“获取他们所有的提交”,而是“获取他们拥有但我们没有的提交”。由于提交具有全局唯一的 ID,我们可以很容易地告诉我们(比如说)提交 a123456 因为我们有一些 ID 为 a123456 的对象,并且我们缺少——因此需要——b789abc因为我们没有这样的ID。获得他们的 new-to-us 提交后,我们的 Git 现在更新相应的 remote-tracking 名称。

换句话说,git fetchgit clone 做的事情几乎相同,只是 我们的 Git 存储库已经存在,我们可能得到的数据要少得多,而且我们没有最后的“创建分支并检查它”的步骤。由于我们可以有多个 remote,我们可以 运行:

git fetch origin

并更新我们所有的 origin/* 名称,然后 运行:

git fetch upstream

并更新我们所有的 upstream/* 名称,如果我们使用 git remote add 添加一个名为 upstreamsecond 遥控器。

要一次更新所有我们的遥控器,我们可以使用git fetch --allgit remote update;两者本质上做同样的事情。请注意,--allgit fetch 表示 所有遥控器 ,而不是 所有分支: 我们已经获得了所有分支。 (我提到这个是因为人们一直认为 --all 意味着 所有分支 而它从来没有。)

如果我们愿意,我们可以限制我们的git fetch像这样:

git fetch origin main

这让我们的 Git 像往常一样打电话给他们的 Git 并列出事情,但是这次,我们的 Git 只麻烦 询问 他们在 main 上的任何 new-to-us 提交。当一切都完成后,我们的 Git 然后更新我们的 origin/main (我们知道 originmain 现在在哪里,所以我们相应的 remote-tracking 名称,即, origin/main,可以更新)。如果他们在 dev 上有新的提交,我们不会得到它们,我们也不会更新我们的 origin/dev;我们的Git被告知只需要main

在一些(罕见的)设置中,这种事情可以节省大量数据传输。 Git 因此提供了一种叫做 single-branch 克隆 的东西,其中 git fetch 通过 default 来做到这一点。这是人们尝试使用 --all 的地方(但它不起作用):要从 single-branch 克隆中获取其他分支,您必须添加它们——参见 the git remote documentation——或使用显式 refspec。不过,出于 space 的原因,我们不会在这里适当地介绍 refspecs。

由于您将有 两个 遥控器,一个用于您的 GitHub 分支,另一个用于您分支的 GitHub 存储库,您将想要 运行 git fetch 两次,或者偶尔使用 git remote updategit fetch --all。除此之外 - 并且拥有 upstream/*,如果你像大多数人一样调用第二个远程 upstream - 你的存储库仍然像任何其他存储库一样。

git push

git push 命令与 git fetch 非常相似,有几个主要区别:

  • 首先当然是git push表示发送东西。您使用 git fetch 获取新提交 (和其他内部对象) 来自 一些其他 Git (一些其他软件工作与其他一些存储库).. 你使用 git push 发送 新的提交,通常是你所做的——但它们 可以是 你的例如刚从 upstream 到其他 Git.

  • 其次,一旦您发送了这些提交,您通常会要求 其他 Git 设置 它的分支名称之一。在推送方面没有这样的东西,比如 remote-tracking 名称。

最后一部分意味着您必须权限才能写入存储库。 Git 本身根本没有真正的访问控制,但大多数网络托管网站,包括 GitHub,都添加了他们的访问控制。 GitHub 在这里特别添加了很多花哨的控件。您 and/or 其他人是否使用它们取决于您和他们。

要执行 git push,您通常 运行 一个简单的:

git push <remote> <name>

这表示您希望 Git 查看您名为 name 的分支上的提交,找到哪些提交origin 的另一个 Git 的新手,将 发送给 Git 的 ,然后礼貌地询问他们是否愿意,拜托,设置 他们的 名称 name 指向与您的 name[=388= 相同的提交]指向。

换句话说,您要求他们创建或更新与您的分支同名的分支。一般来说,他们会接受这个 当且仅当 这只是 添加 到他们的分支(当然你有权限)。也就是说,当我们有:

...--G--H   <-- main (HEAD), origin/main

因为我们的 main 与来源的 main 匹配,我们添加了一两个新的提交:

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

我们 运行 git push origin main,我们的 Git 调用他们的 Git,向他们发送提交 I-J,并要求他们设置他们的 main 指向 J.

如果他们的 main 仍然指向 H——或者不知何故,指向 G 因为有人让他们 drop H—他们会很高兴地接受我们的请求来添加到他们的 main。由于我们的 Git 看到他们接受了,我们最终得到:

...--G--H--I--J   <-- main, origin/main

知道 originmain 现在指向提交 J

但是假设其他人出现并添加了一些提交 K他们的 main:

...--G--H--K   <-- main [over on origin]

我们的请求现在将要求他们放弃他们的提交K,这将给他们留下:

...--G--H--I--J   <-- main
         \
          K   ???

他们会说 no,您将收到的错误消息是 not a fast-forward(记住那些来自合并? 这是同一个想法)。

您可以使用 --force--force-with-lease,尝试让他们接受更改,丢失他们的新提交,但是 通常 这是错误的要做的事。然而,对于 GitHub 的使用,有时这是 正确 在你的分叉上做的事情!我们稍后再谈这个。

还有一种方法可以使用 git push 删除名称。其实有好几个,不过最清楚的大概就是git push --delete <em>remote branch</em>: git push --delete origin foobranch would在 origin 上丢弃 foobranch。这对您的笔记本电脑存储库没有影响。

Git集线器“分支”

我们现在有足够的背景来定义 GitHub 的 FORK 按钮是如何工作的。您选择一些不属于您自己的现有存储库并单击它,GitHub 将在 GitHub 上创建一个新存储库 你自己的。这个 GitHub“fork”一种克隆,但有几个添加的功能和一个变化。

既然您知道 git clone 复制 没有 分支,那么变化就很明显了。当您使用 GitHub 的 fork 按钮时,它会复制 all 个分支。您的新克隆与原始克隆具有相同的提交和分支集,而常规 clone-to-your-laptop 获得所有提交,但分支的 none,并且然后创建一个 new 分支,使 accidentally-on-purpose 与 origin 的分支之一完全匹配。 fork 按钮使 all 您的 fork 中的分支名称与所有其他存储库的分支完全匹配。

添加的功能包括提出 拉取请求 的想法,我们稍后会回过头来。在 GitHub 方面——你不可见,但对 GitHub 本身非常重要——添加的功能包括不使用任何 space 来保存提交:你的 fork 只是 re-uses 来自原始的提交。没有提交可以改变,所以这很好;唯一可能发生的问题是如果要删除提交,因此 GitHub 只是安排永远不会删除提交。1

不过,一旦你做了分叉,那些分支名称,在你分叉的 GitHub 上,不要再更新,直到并且除非 去做吧。您可以从 GitHub 的 Web 界面执行一些操作(例如,您可以删除分支名称),或者您可以像往常一样在笔记本电脑上使用 git push

因此,一旦您 有了叉子,您就会想将该叉子克隆到您的笔记本电脑上,然后, 笔记本电脑上, 添加第二个 URL 到你分叉的存储库。标准的 GitHub 命名方式 URL 是使用远程名称 upstream。我个人不喜欢这个名字,因为 upstream 这个词在 Git,2 中已经有多种含义,但是 运行 ,如果你将 ssh://github.com/them/repo.git 分叉到 ssh://github.com/you/repo.git,你会 运行:

git clone ssh://github.com/you/repo.git
cd repo
git remote add upstream ssh://github.com/them/repo.git
git fetch upstream

您现在有 origin/*upstream/* 个名字。我们现在来了解其中一个方便的技巧。


1这意味着如果有人不小心将密码放在 GitHub 上,它可能永远存在,即使他们很快 force-push 隐藏它。 GitHub 支持可以“真正”清除提交,但总的来说,始终考虑任何意外暴露的秘密,即使是一瞬间,也会永远受到损害。

2所以,至少比branch这个词好。


小窍门:更新你的叉子

拥有 运行 git fetch upstreamgit remote update 以便您的 upstream/* 名称全部更新,您可能希望让您自己的 fork 拥有他们的所有更新相同的 分支 名称。这意味着对于每个 upstream/whatever,您想要 运行:

git push origin upstream/whatever:whatever

这种 git push 使用 refspec,我们在左侧放置一个“来源”名称,然后是一个冒号,然后是“目的地”右边的名字。 Git 将从给定的源(我们的本地 upstream/whatever remote-tracking 名称)中提取提交,但是当它们到达目的地(origin)时,要求目的地进行设置他们的destination-side名字(他们的whatever)。

可以使用循环来完成此操作,但还有更短的方法。请注意,您可能需要保护 * 字符免受 shell 的影响,具体取决于您的特定命令行解释器:

git push origin "refs/remotes/upstream/*:refs/heads/*"

我假设您需要双引号才能获得正确的保护。如果您不能使用双引号,请使用 所需的任何引号机制(可能根本就是 none)。

这里,我们把名字全拼出来了:remote-tracking名字在refs/remotes/name-space,而分支名字在refs/heads/。 Git 匹配两颗星并在此处执行 每个 分支的常规 (non-forced) git push

您可以制作一个 Git 别名来执行此操作 git push,以避免必须键入长命令并避免引用 refspec(一个简单的 Git 别名可以不通过shell):

[alias]
    up2hub = push origin refs/remotes/upstream/*:refs/heads/*

请注意,这会嵌入名称 upstreamorigin,但现在您可以 运行:

git up2hub

git fetch upstream 成功后,更新您的 GitHub 分支。

拉取请求

现在我们终于到了问题的核心:拉取请求是如何工作的。 当您使用 CREATE PULL REQUEST 按钮时GitHub,你选择两个东西,尽管 GitHub 会为你默认其中一个:

  • 你的分支中的一个分支名称;和
  • other GitHub 存储库3 中的一个“基本分支”,您希望对其进行 PR。

GitHub 现在将 运行 进行“测试合并”,他们尝试对 您的 [=] 上的一组提交进行常规 git merge =388=] 分支,通过合并请求导入到他们的存储库,到 他们的 分支的当前提示提交。也就是说,GitHub 获取你在 fork 中的每个提交,而他们在他们的存储库中根本没有,然后 将这些提交复制到他们的存储库 4

他们现在可以在 pull-request 的全名下找到您的提交,在 GitHub 上是 refs/pull/<em>人数</em>/头。测试合并,如果它有效,将创建一个新的提交并使其可以通过 refs/pull/<em>number</em>/merge 访问。如果它因合并冲突而失败,PR 仍然会生成,只是没有refs/pull/<em>number</em> /merge 名称。


3您可以向 共享访问 存储库发出拉取请求,您和其他人都将推送到单个存储库,而不是每个到他们自己的叉子。在那种情况下,您会选择此存储库本身作为“其他”Git 存储库。但这只是一个特例,“其他”存储库是“这个”。

4这一切都是虚拟发生的,为了节省磁盘 space:他们的存储库有一个 link 回到你的,所以没有实际的复制,就像当你分叉他们的 repo 时,你没有真的 复制他们的提交。同样,Git 开始使用每个提交都有一个唯一的哈希 ID 的事实:您的提交和您的哈希 ID 保证与所有提交具有不同的哈希 ID。因此,当他们的 Git 软件试图找到提交 fee1cab 或任何哈希 ID,但找不到时,他们可以在您连接的分支中查看,它就在那里。你的叉子指回他们的回购,他们的回购回指回你的叉子,在一种乱伦循环中。


那么这一切意味着什么?

好吧,我们来看一个经典的例子。您 fork 一些存储库,并将其克隆到您的笔记本电脑并创建一个新分支:

...--G--H   <-- main, my-feature-1, origin/main

您在 my-feature-1 分支上做了几个新的提交:

          I--J   <-- my-feature-1 (HEAD)
         /
...--G--H   <-- main, origin/main

您将这些提交发送到您的 GitHub 分支:

          I--J   <-- my-feature-1 [on your fork]
         /
...--G--H   <-- main [on your fork]

然后单击按钮进行 PR,在 他们的 分叉中,他们现在拥有:

            I--J   <-- refs/pull/123/head
           /    \
          /      M   <-- refs/pull/123/merge
         /      /
...--G--H---K--L   <-- main

提交 M 是 GitHub 的“测试合并”,有效; commits K-L 是他们在你忙于你的叉子和你的笔记本电脑时所做的新提交。

如果你现在继续制作:

          I--J   <-- my-feature-1, my-feature-2 (HEAD)
         /
...--G--H   <-- main, origin/main

在你的笔记本电脑上,然后进行两次 more 提交,你得到:

               N--O   <-- my-feature-2 (HEAD)
              /
          I--J   <-- my-feature-1
         /
...--G--H   <-- main, origin/main

你可以 git push 这些到你的 GitHub 分支,这样它就有:

               N--O   <-- my-feature-2 [on your fork]
              /
          I--J   <-- my-feature-1 [on your fork]
         /
...--G--H   <-- main [on your fork]

如果您现在使用此 my-feature-2 name 在 GitHub 上制作 PR ,则该 PR包含尚未在 main 中的提交 I-J-N-O,因为他们尚未决定如何处理 PR#123:

                 N--O   <-- refs/pull/124/head
                /
            I--J   <-- refs/pull/123/head
           /    \
          /      M   <-- refs/pull/123/merge
         /      /
...--G--H---K--L   <-- main

(如果他们能够将 OL 合并,或许还有一个测试合并)。

如果那不是你想要的,你 应该放在 GitHub 上的是:

          I--J   <-- my-feature-1 [on your fork]
         /
...--G--H   <-- main [on your fork]
         \
          N--O   <-- my-feature-2 [on your fork]

你的 my-feature-2 现在只包含 两个 提交不在他们的 main 中,所以 PR#124 使他们的存储库看起来像这样:

            I--J   <-- refs/pull/123/head
           /    \
          /      M   <-- refs/pull/123/merge
         /      /
...--G--H---K--L   <-- main
         \      \
          \      P   <-- refs/pull/123/merge
           \    /
            N--O   <-- refs/pull/124/head

第 3 部分:您的下一个绊脚石:变基

(; )

当他们(无论他们是谁)开始审查您的 PR 时,他们可能会:

  • 接受原样;
  • 请你修复里面的东西;
  • 接受他们所做的改变;或
  • 完全拒绝。

最后一个在这里不需要更多讨论,但其他三个需要。

如果他们“按原样”接受你的提交,他们可以选择使用三个不同的“接受这个 PR”MERGE 按钮, 在 Git 中心:

  • 合并。这个很简单。
  • 变基并合并。这个没那么简单。
  • 压缩并合并。这个需要理解Git的“squash merge”,它根本不是合并。

如果他们自己进行任何更改,那很像 REBASE AND MERGE 的情况,正如我们将要看到的那样。如果他们想要 you 进行更改,you 将需要在您的笔记本电脑上使用 git rebase,之后您将需要使用 git push --forcegit push --force-with-lease 更新您的 GitHub 存储库。5


5从技术上讲,您可以删除并 re-create 您的分支,而不是 force-pushing。不过,我认为这会破坏现有的 PR(我还没有尝试过)。在任何情况下,force-push 选项都是人们在实践中使用的选项。


Rebase 是关于复制 提交

我们使用 git rebase 将现有的提交复制到我们使用 而不是 原件.

在我们看那个之前,让我们看一下复制一个提交的命令。正如我们所知,任何提交都不能 更改 ,但我们始终可以提取提交,或将其转换为差异,或其他任何方式。我们可以使用这个 属性 来获取一些现有的提交,将其转化为更改,然后再次应用这些更改,或应用到其他地方。 Git 调用此操作 cherry-picking,并使用命令 git cherry-pick 来执行此操作。

通常,出于某种原因,我们可能会使用 git cherry-pick 作为获取提交的 one-off 副本的快速方法。例如,也许有人有一个好主意或错误修复,我们现在需要 我们的 分支上,我们会想办法来处理这将在以后造成的混乱。我们在我们的分支:

...--J--K   <-- feature (HEAD)

与此同时,在他们的分支上,他们修复了一些困扰我们的讨厌的小错误:

...--P--C--R--S   <-- theirs

(我们将在 P 之后调用 fixing-commit,“parent” 提交,C,“child”,此处,而不是我通常使用的字母 Q)。我们运行:

git cherry-pick <hash-of-C>

告诉Git:去弄清楚whoever-it-is在提交C中做了什么,P的child,通过比较 PC 看看有什么变化。然后在我的提交 K 上进行 相同的 更改,并从中进行新的提交。 结果图如下所示:

...--J--K--C'  <-- feature (HEAD)

Git 会将他们列为提交 C' 的作者,以及 re-use 他们的提交信息;我们将被列为 C' 提交者 。 (大多数时候,作者和提交者是同一个人,但不一定;像这样的副本通常将他们保留为作者,并保留他们的提交 date-and-time 邮票。)

Git 实际实现此“复制他们的更改”的方式是 Git 必须弄清楚要触摸哪些文件以及这些行去了哪里。为此,Git 从 PC 进行比较,以查看 他们 做了什么,然后从 [=39 进行第二次比较=] 到 K 看看 我们 做了什么。回想一下 git merge 是如何工作的:这是合并的核心: 合并两个差异并将合并后的差异应用于合并基础 。 Git 只是强制提交“基础”P,不管其他。我们的提交是提交 K,他们的提交是提交 C,仅此而已——除了当 Git 提交合并时,它使它成为一个普通的 single-parent 犯罪。提交 C' 仅指回 K,而不是 PC.

最终结果,如果一切顺利——如果没有合并冲突——我们有来自提交 C更改,但应用在这里,在提交 K。因此,新提交 C'C 副本,因为它:

  • 一些C 相同的元数据:特别是日志消息和作者身份;和
  • 具有相同的 差异 ,除了在我们必须解决合并冲突时添加或删除的任何内容。

有了git rebase,我们可以:

  • 采取一系列现有的提交并简化移动他们,或者
  • 使用 interactive rebase,做各种额外的摆弄。

Interactive rebase 是一个很大的 blog-post 或单独的一篇文章,我们不会在这里详细介绍。我们只看一个简单的 rebase-to-move 作业。我们开始于,例如:

          I--J   <-- feature (HEAD), origin/feature
         /
...--G--H--K--L   <-- upstream/main

假设,在这一点上,我们喜欢一切关于I-J除了他们不会追赶K-L。让我们再添加一个问题:存在 合并冲突 ,例如,因为我们在 J 中接触的其中一条线与我们(或他们)在 [=] 中接触的其中一条线相邻70=]。这个合并冲突,或者 Git 认为是冲突的事情,很容易解决,但是 Git 不会做,所以我们必须自己做。

此时,我们只是运行:

git rebase upstream/main

请注意,我们不必在这里使用 分支 名称,我们甚至不必更新 origin:我们可以在任何提交上变基我们已经为该提交使用任何名称。 name upstream/main 找到 commit L,这就是我们要复制 IJ 到 come-after 的 commit,所以这就是我们给 git rebase 这里.

Git 将在内部保存提交 IJ 的原始哈希 ID,它们是要复制的提交。 (Git 如何知道这一点——以及我们如何改变被复制的内容,到哪里——在别处得到回答,但请注意我们的 当前分支名称 指向 J并且提交 I-J 是唯一可以从当前分支 访问的提交。)然后,Git 将切换到提交 L——那个我们命名了——在 Git 所谓的 分离 HEAD 模式中。这里根本没有当前分支: HEAD 直接指向当前提交。所以现在我们有了这个:

          I--J   <-- feature, origin/feature
         /
...--G--H--K--L   <-- upstream/main, HEAD

Git 现在做一个 cherry-pick 复制 I。这行得通,并且 Git 进行了新的提交 I':

          I--J   <-- feature, origin/feature
         /
...--G--H--K--L   <-- upstream/main
               \
                I'  <-- HEAD

Git 现在尝试第二次 cherry-pick 复制 J。这个因 合并冲突 而失败。我们通过在编辑器中打开冲突文件,将 正确的合并结果 放入文件,并将文件写回我们的工作树来解决冲突,然后 运行ning git add 在那个文件上。然后我们使用:

git rebase --continue

使 Git 恢复 committing-and-copying-and-so-on。 Git 为 J' 提交(副本,我们的决议到位,如 git add-ed):

          I--J   <-- feature, origin/feature
         /
...--G--H--K--L   <-- upstream/main
               \
                I'-J'  <-- HEAD

如果有更多提交要复制,Git 会尝试 cherry-picking 下一个。然而,事实上,copy 已经没有什么可做的了,所以 git rebase 进行最后的操作:

  • rebase 将名称 feature 拉到这里,无论 HEAD 在哪里;和
  • 变基 re-attaches HEAD

所以我们现在有:

          I--J   origin/feature
         /
...--G--H--K--L   <-- upstream/main
               \
                I'-J'  <-- feature (HEAD)

原来的I-J提交仍然存在。如果您记下了它们的哈希 ID(或者它们仍然在您的屏幕上),您仍然可以看到它们。使用名称 origin/feature,您仍然可以看到它们。但是,如果您使用 name feature 查找提交,您将找到新的副本 而不是 原件。

更新拉取请求

要更新我们现有的拉取请求(可能是 PR#125,feature 到我们分叉的存储库中的分支名称),我们只需告诉 GitHub 接受这两个新提交。一个普通的:

git push origin feature

不会工作,因为 Git 在 GitHub 会 object: 嘿,如果我更新我的 feature,我会失去这两个非常有价值的 I-J 提交!不是快进! 拒绝!我们必须强制它更新,使用新的替换I'-J' 6 所以我们 运行:

git push --force-with-lease origin feature

或更短的 --force 变体。 (--with-lease 增加了一些错误检查,这是个好主意,但仍然感觉很新奇,至少对我来说打字很笨拙。这是 15+ 使用 Git 的缺点之一年。)我们告诉 GitHub 我们是认真的,他们接受了新的提交。

因为有一个公开的 PR 引用 name feature,GitHub 将在此时再次尝试合并。他们上次尝试此合并时,与提交 L 发生合并冲突。这一次,我们 添加到他们的提交 L,因此没有合并冲突,并且 PR 可能会被接受 as-is.

如果我们需要进行额外的更改,我们可以使用花哨的交互式 rebase,或者做额外的提交然后压缩,或者任何我们喜欢的。


6如果Git知道这些是进化的替代品就好了。


挤压

Git 提供它拼写的内容 git merge --squash。这不是合并,就像 fast-forwarding 不是合并一样:没有最终的 merge commit。但它 合并,就像 git merge 做一对差异和组合工作一样:这是 combining-work 部分。

给定:

...--G--H--I--J   <-- br1 (HEAD)
         \
          K--L   <-- br2

如果我们 运行 git merge --squash br2, Git 将:

  • 像往常一样找到合并基地H; * 像往常一样做两个差异;
  • 合并差异,如果存在合并冲突则停止并让我们修复混乱;或
  • 如果没有冲突,无论如何都停止。

我相信,这个“无论如何停止”只是最初实现的一个意外——git merge 有一个 --no-commit 标志来 使 它停止,它应该与 --squash 分开,但 --squash 始终打开 --no-commit。但是,无论如何,我们现在必须通过 运行ning git commit 完成操作,这会像往常一样提交 Git 的索引和您的工作树中的内容,并且 不进行合并提交。不是合并 M 两个 parent,我们得到一个简单的普通提交 S——“挤压合并”——一个 parent 像往常一样:

...--G--H--I--J--S   <-- br1 (HEAD)
         \
          K--L   <-- br2

新提交 S 中的 快照 与我们 拥有的快照相同,如果我们做了常规合并,但 S 不会 link 回到 L,现在唯一 有用的 与名称 br1 相关就是 删除 它。7 这两个提交 K-L 实际上已经合并,或者 squashed,进入单个提交 S。提交 SK-L 具有相同的 效果 ,因此 K-L 现在无用,应该被遗忘。


7也可以这样做,但这超出了这个答案的范围。


squash-merge 如何与 GitHub PR

相关

如果上游有人接受你的 PR 并且 squash-merge 它,你给他们的东西——可能是多次提交——现在被一个单一的压缩提交所取代:

          I--J   <-- refs/pull/125/head
         /
...--G--H--K--L--S   <-- theirbranch

在这里,他们的提交 S 代表您在 I-J 中所做的 工作。你所有的实际工作,以及至少你的一些提交日志消息,都被他们的 squash 所取代(他们可能会或可能不会保留你的一些提交日志消息)。您应该获得提交 S(进入您的 upstream/theirbranch)并使用它,放弃您的 I-J 原件。

rebase-and-merge 对您的 PR 有何影响

如果有人在上游获取您的 PR 并使用 REBASE AND MERGE 按钮,GitHub 的 Git 软件将 复制每个您的提交 到 new-and-?改进了吗?提交。他们会这样做即使没有真正的需要。例如,您可能已经仔细地将 I-J 重新定位到他们的 L 上,这样您就可以:

                I'-J'  <-- refs/pull/125/head
               /
...--G--H--K--L   <-- theirbranch

在你发布 PR#125 的时候。但是他们点击了“错误的”8 按钮,因为他们喜欢线性图,所以现在在 他们的 存储库中他们有:

                I'-J'  <-- refs/pull/125/head
               /
...--G--H--K--L--I"--J"  <-- theirbranch

其中 I"I' 的副本,J"J' 的副本。这些副本保留了您的原始日志消息,并且您是作者,但它们具有新的和不同的哈希 ID。

您需要放弃 您的 原始提交以支持这些“新的和改进的”提交。不过,git rebase 中有一个好东西——另一个方便的技巧——可以让你更轻松。


8我只称这是“错误的”,因为它不必要地重复了提交。对于你的 PR 挂起提交 H 的情况,提交 did 实际上 必须 重新设置基线,以使图形线性化。但是,效果是您是作者,他们是提交者,并且这些提交具有新的和不同的哈希 ID。


小窍门:变基知道副本

当有人做了这种“rebase and merge”,你几乎总是可以用git rebase yourself to replace 你的原始提交——即使你自己对它们进行了多次变基——以及它们的新变基副本。原因是当 Git 去列出对 copy 的提交时,它会检查这些提交是否 已经在 这个地方您正在将 复制到 。也就是说,假设 在你的笔记本电脑上有这个:

               K--L   <-- feature2 (requires feature)
              /
          I--J   <-- feature
         /
...--G--H   <-- upstream/theirbranch

你现在用 feature 做了一个 PR,他们按原样接受它,但使用了“错误”按钮,所以你的 git fetch upstream 结果是这样的:

               K--L   <-- feature2 (requires feature)
              /
          I--J   <-- feature
         /
...--G--H--I'-J'  <-- upstream/theirbranch

您现在必须在提交 J' 之后进行提交 K'-L'

可以 明确地执行此操作 (git switch feature2; git rebase --onto upstream/theirbranch feature),但是:

git switch feature2
git rebase upstream/theirbranch

会完成这项工作。原因是 Git 首先列出了要复制的四个提交 I-J-K-L,然后查看提交 I'-J' 并且 发现这些是 [=74] 的副本=] 和 J。 rebase 代码使用它 drop 完全提交 I-J,导致:

          I--J   <-- feature
         /
...--G--H--I'-J'  <-- upstream/theirbranch
                \
                 K'-L'  <-- feature2 (HEAD)

事实上,如果您还 运行 git switch feature 然后 git rebase upstream/theirbranch,您的 Git 将简单地 drop 提交 I-J从复制过程中,离开你用这个:

...--G--H--I'-J'  <-- upstream/theirbranch, feature (HEAD)
                \
                 K'-L'  <-- feature2

如果有人必须手动修复至少一个提交,这个不会(相当)有效。在过去,在 GitHub 获得一些额外的工具之前,这永远不会直接在 GitHub 上发生。现在(他们有这些工具)至少在理论上是可能的。