我在 GIT 上的拉取请求包含其他人完成的几个提交。如何防止这种行为?

My pull request on GIT contains several commits done by others. How to prevent this behavior?

我从分支 A 创建了一个分支(分支 B)并进行了更改并将其推送到分支 B。然后我尝试向分支 A 发出拉取请求。但是,我的拉取请求包含之前的几个提交其他人(其他团队成员)。为什么会这样?我怎样才能提出一个只包含我的提交的干净的 PR?

编辑: 我使用的托管站点是 Bitbucket。

初步说明:我实际上并没有使用 Bitbucket,我的一些术语可能略有偏差。具体来说,我可能会在这里使用一些仅在 Bitbucket 上找到的术语。但 Bitbucket 和 GitHub 处理分叉和拉取请求 (PR) 的方式非常相似,幸运的是,似乎大约 6 或 7 年前 Bitbucket 更新了他们的 PR 机制以匹配 GitHub 的(参见 How to update a pull request on bitbucket?).

我也不确定您是在使用 Bitbucket 分支,还是直接在具有单独分支的共享存储库中工作。我打算假设一个叉子,但这应该在共享存储库中工作相同(它在 GitHub 上)。

I created a branch (branch B) from branch A and did my changes and pushed it to branch B. Then I tried to make a pull request to branch A. But, my pull request contains several previous commits done by others (other team members). Why is this happening?

Git 级别,我们真正关心的是提交。这些是 Git 处理的实体,一次一个完整的提交。每次提交都需要了解三四件事,具体取决于您如何计算这些项目:

  1. 每个提交都有一个唯一的哈希 ID。这是你在 git log 输出中看到的丑陋的大数字(40 个字符,对于 SHA-1)hexadecimal。这个哈希 ID 实际上是提交的“真实名称”。我们使用的任何分支或标签或其他名称都只是让一些 Git 存储库 找到 正确哈希 ID 的方法。每个 Git 存储库——每个分支或克隆——都有 自己的私有名称。托管站点上的分叉机制提供了一种将一个托管存储库连接到另一个托管存储库并从另一个托管存储库中查看部分或全部名称的方法,但始终是 哈希 ID 才是真正重要的。

  2. 提交由快照和元数据组成。从技术上讲,快照 是提交本身中的 元数据,但让我们首先考虑快照。每个快照都包含与该特定提交一起使用的每个文件的完整副本。这里没有 更改 ,只有完整的快照。这对于任何给定的 PR 都不是太重要,但是当我们去制作提交的副本时很重要。

  3. 任何给定提交中的 元数据 都会提供诸如作者和提交者(姓名、电子邮件和 date-and-time 元组)和日志消息之类的信息.也就是说,元数据包含提交的描述。任何一次提交中的元数据也有一个 父提交 哈希 ID 列表。大多数提交——那些 Git 称为 普通 的提交,如果根本不屑于给它们任何形容词的话——只有一个父哈希 ID。这是就在我们现在正在查看的提交之前的(单个)早期提交。

  4. 任何提交的所有部分都是完整的 read-only。这就是哈希 ID 的工作原理:哈希 ID 只是提交全部内容的加密校验和。 date-and-time-stamp 通常使提交本身独一无二,但即使您设法在一秒钟内进行多次提交,或者伪造时间戳,父提交哈希 ID 存储在 this 提交与存储在 parent 提交中的父提交哈希 ID 不同,仅此一项就可以为该提交提供唯一的哈希 ID。

(第 4 项可能是第 1 项的一部分,具体取决于您希望如何处理它。请注意,Git 通过哈希 ID 查找 提交,然后 在从数据库中提取提交对象时验证哈希 ID 是否与存储数据的校验和相匹配。这会检测到哪怕是一点点的更改都是损坏的提交,因此任何部分都不会提交可以永远改变。)

父哈希 ID 最终将提交串在一起作为 backwards-looking 链。我们可以相当简单地绘制它,假设是普通提交,通过使用大写字母代表实际的哈希 ID。如果我们把最新的提交,H,放在右边,我们会得到一个看起来像这样的图:

... <-F <-G <-H

提交 H 包含所有文件作为其快照。它在其元数据中的(单个)父哈希 ID 中包含早期提交的实际哈希 ID G。 Git 因此可以使用 H 的元数据来查找提交 G。这也有一个完整的快照,通过比较 GH 快照,Git 将找到我们在提交 H 中更改的内容。通过使用 G 中的元数据,Git 可以再返回一步,提交 F.

Commit F 当然也有快照,并且有自己的元数据和另一个父哈希 ID。通过向后跟踪每个提交,一次一个跃点,Git 可以返回到任何人为此存储库所做的第一次提交。该提交的特殊之处在于它没有没有父哈希ID,这使得它成为Git所谓的根提交。 Git 现在可以停止倒退,访问了导致提交 H 的每个提交。这 存储库中的历史记录,至少对于最后一次提交为 c 的分支而言mmit H.

Git 需要 提交的哈希 ID H 来完成所有这些。要得到那个哈希ID,Git可以使用你的分支名称。例如,如果 H 是分支 master 的最后一次提交,我们可以这样绘制:

...--G--H   <-- master

名称master 指向(定位)数据库中的提交H;提交 H 指向 G;等等。由于我绘制分支的方式,我此时停止绘制箭头:

...--G--H   <-- master
         \
          I--J   <-- develop

这里,名字develop指向JJ 指向 II 指向 HJ 之前的提交在开发中,H 之前的提交在 master。这意味着许多提交都在 both 分支上。如果您习惯于其他版本控制系统,那是关于 Git 的一件奇怪的事情:提交在分支中出现和消失取决于您放置 分支名称 .[=117 的位置=]

重要的不是分支名称! 提交。分支名称并非 完全 无关,因为我们使用它们来 查找 提交,但它们 大部分 无关紧要,因为我们可以随心所欲地改变它们。例如,GitHub 现在使用 main 而不是 master,如果您喜欢这个,只需将 master 重命名为 main,现在所有 master 提交都是 main 提交。在这个特定的例子中,我们甚至可以 完全删除 master ,只留下 develop。这已经足够好了,因为提交 I 导致向后提交 H.

这不是人类通常认为分支的方式

当我们看这样的图表时:

          I--J   <-- branch1
         /
...--G--H   <-- master
         \
          K   <-- branch2
           \
            L   <-- branch3

很多人类会说通过H的提交在master上,I-J是在[=上的提交65=],Kbranch2 上的唯一提交,Lbranch3.

上的唯一提交

不是Git对待他们的方式。通过 H 的提交在 所有四个 分支上,提交 K 在两个分支上。剩下的三个提交都只是一个分支。为了让 Git 和人类就这些事情达成一致,我们最终做的是使用 排除规则 的形式:

master..branch1

这实际上意味着:J 可到达的所有提交集减去从 H 可到达的所有提交集。这给了我们 I-J 对。同样地,master..branch2 让我们只提交 K,而 branch2..branch3 让我们只提交 L.

分支名称不是唯一的名称类型

除了 branch 名称之外,Git 可以找到许多其他类型名称的提交。例如,在 GitHub 上,pull requests 导致名称格式为 refs/pull/<em>number</em> /head 出现在对其进行 PR 的存储库中。这个特定的名称在 GitHub 上链接到某个存储库中的某个分支名称——例如,你在复刻中的分支名称,或者通过共享在同一存储库中的分支名称。

(Bitbucket 使用的名称略有不同,但概念一致。)

你的情况

我们看不到各个存储库中的各种名称,但是我们可以大概知道。您自己有时只能看到 一些 这些名称,这取决于很多因素(包括您是否进行了分叉,以及托管站点的规则)。但是我们知道,从你抱怨别人的提交包含在你的 PR 中,PR 要求某人添加提交的存储库中的实际情况看起来有点像这样:

...--G--H   <-- master
         \
          I--J   <-- someone-elses-work
              \
               K   <-- your-PR

您所做的一次提交,提交 K在其他人所做的两次提交 之后:I-J。您的拉取请求要求他们——无论“他们”是谁——将你的提交 K 合并到 他们的分支 master 中。 GitHub 将其称为拉取请求的 base 分支

因为无法更改提交,所以他们只能通过添加全部三个 提交来添加提交K。所以你的 PR 要求添加所有这些提交。

How can I raise a clean PR which contains only my commits?

您必须进行新的提交。

[According to the accepted answer to I have to create a new branch, cherry-pick commits from my old branch, and raise a [new] pr

这些特定的 步骤 不是必需的,特别是 Bitbucket 不再需要“提出新的 PR”(但早在 2014 年)。然而,总体想法大体上是正确的。

假设我画的图是准确的(或足够接近),提交 K 问题。它添加到提交 J。相反,您想要一个添加到提交 H 上的提交。为此,您 必须 进行新的和改进的提交——换句话说 cherry-pick。

让我们 re-draw 在 你的 存储库中的分支图,t嗯,像这样:

...--G--H   <-- master
         \
          I--J   <-- branch-X
              \
               K   <-- feature (HEAD)

(括号中的额外 HEAD 表示您当前的分支名称是 feature,因此 K 是您当前的提交)。

可以创建一个指向提交H的新分支名称,然后进入该分支:

...--G--H   <-- feature2 (HEAD), master
         \
          I--J   <-- branch-X
              \
               K   <-- feature

然后cherry-pickK复制一份,我们可以称之为K':

          K'  <-- feature2 (HEAD)
         /
...--G--H   <-- master
         \
          I--J   <-- branch-X
              \
               K   <-- feature

然后您可以完全删除 feature,将 feature2 重命名为 feature,并使用 git push -f 更新您的 Bitbucket 分支或共享的 Bitbucket 存储库,这将使 Bitbucket 自动更新您的 PR。

或者,您可以简单地 运行:

git rebase --onto master branch-X

(从未创建 feature2)。您的 Git 将:

  • 使用detached HEAD模式,像这样提交H:

    ...--G--H   <-- HEAD, master
             \
              I--J   <-- branch-X
                  \
                   K   <-- feature
    
  • 列出了提交K的哈希ID(内部使用branch-X..feature1),cherry-pick提交K得到K';最后

  • 强制名称 feature 指向 K'

最后的结果是:

          K'  <-- feature
         /
...--G--H   <-- master
         \
          I--J   <-- branch-X
              \
               K   [abandoned]

但您不需要太多的动作——例如,没有单独的 git checkoutgit cherry-pick 操作——也没有必须删除旧的 feature 然后再删除的轻微痛苦重命名临时分支名称,以及丢失任何关联上游的相关头痛等。所以这绝对是一个更user-efficient到达你想去的地方的方式。


1实际上,git rebase 的内部结构比这复杂得多。有 fork-point 魔术,--no-merges 部分,对称差异和 cherry-mark / git cherry upstream-equivalent 省略。但是 stop..start 的东西是从哪里开始的,对于这种情况,none 的复杂性应该成为障碍。尽管如此,这就是教程应该以 cherry-pick 而不是 rebase!

开头的原因

“基础分支”很重要

我不知道 Bitbucket 怎么称呼它,但是在提出 PR 时,“基本分支”是 GitHub 确定您实际要求合并的提交范围的方式(通过指定合并目标分支名称)。 GitHub 的机制过于复杂,他们拒绝显示实际提交 graph,这使得这非常棘手。您可以更改 PR 的基础分支;这样做的效果也很棘手:我不清楚,例如,如果你移动到后代提交,然后回到原始提交,甚至是它的祖先之一,会发生什么。