我从分支 A 创建了一个分支(分支 B),并进行了更改并将其推送到分支 B。然后我尝试向分支 A 发出拉取请求。但是,我的拉取请求包含其他人(其他团队成员)完成的几个先前提交。为什么会这样?如何提出仅包含我的提交的干净 PR?
编辑: 我正在使用的托管站点是Bitbucket。
初步说明:我实际上并没有使用 Bitbucket,我的一些术语可能略有偏差。 具体来说,我可能在这里使用一些仅在Bitbucket上找到的术语。 但是 Bitbucket 和 GitHub 处理分叉和拉取请求 (PR) 非常相似,幸运的是,似乎大约 6 或 7 年前 Bitbucket 更新了他们的 PR 机器以匹配 GitHub 的机器(请参阅如何在 bitbucket 上更新拉取请求?)。
我也不确定您是使用的是 Bitbucket 分叉,还是只是直接在具有单独分支的共享存储库中工作。 我将假设一个分叉,但这在共享存储库中应该工作相同(它在 GitHub 上确实如此)。
我从分支 A 创建了一个分支(分支 B),并进行了更改并将其推送到分支 B。然后我尝试向分支 A 发出拉取请求。但是,我的拉取请求包含其他人(其他团队成员)完成的几个先前提交。为什么会这样?
在Git级别,我们真正关心的是提交。 这些是 Git 处理的实体,一次一个完整的提交。 每次提交都有三到四件事需要了解,具体取决于您如何计算这些项目:
-
每个提交都有一个唯一的哈希 ID。 这是您在输出中看到的大丑陋(40 个字符,对于 SHA-1)十六进制数
git log
。 实际上,此哈希 ID 是提交的"真实名称"。 我们使用的任何分支或标签或其他名称都只是让某些 Git 存储库找到正确哈希 ID 的方法。 每个 Git 存储库(每个分支或克隆)都有自己的私有名称。 托管站点上的复刻机制提供了一种将一个托管存储库连接到另一个托管存储库并查看其他托管存储库中的部分或全部名称的方法,但真正重要的始终是哈希 ID。 -
提交由快照和元数据组成。 从技术上讲,快照是提交本身的元数据,但让我们先考虑快照。 每个快照都包含与该特定提交相关的每个文件的完整副本。 这里没有变化,只有完整的快照。 这对于任何给定的 PR 都不太重要,但当我们去复制提交时很重要。
-
任何给定提交中的元数据都会提供作者和提交者(名称、电子邮件以及日期和时间元组)以及日志消息等内容。 也就是说,元数据包含提交的描述。 任何一个提交中的元数据还具有父提交哈希 ID 的列表。 大多数提交(Git 称之为普通提交,如果它费心给它们任何形容词的话)只有一个父哈希 ID。 这是(单个)较早的提交,就在我们现在看到的提交之前。
-
任何提交的所有部分都是完全只读的。 这就是哈希 ID 的工作方式:哈希 ID 只是提交全部内容的加密校验和。 日期和时间戳通常使提交本身是唯一的,但即使您设法在一秒钟内进行多次提交,或者伪造时间戳,存储在此提交元数据中的父提交哈希 ID 与存储在父提交中的父提交哈希 ID 不同,仅此一项就可以为此提交提供唯一的哈希 ID。
(第 4 项可能是第 1 项的一部分,具体取决于您希望如何处理它。 请注意,Git 通过其哈希 ID查找提交,然后在从数据库中提取提交对象时验证哈希 ID 是否与存储数据的校验和匹配。 这会将哪怕一位更改检测为损坏的提交,因此任何提交的任何部分都无法更改。
父哈希 ID 最终将提交串在一起,作为向后看的链。 我们可以相当简单地绘制它,假设普通提交,通过使用大写字母来代替实际的哈希 ID。 如果我们把最新的提交,H
,放在右边,我们会得到一个看起来像这样的绘图:
... <-F <-G <-H
提交H
包含所有文件作为其快照。 它在其元数据中的(单个)父哈希 ID 中包含早期提交G
的实际哈希 ID。 因此,Git 可以使用H
的元数据来查找提交G
。 这也有一个完整的快照,通过比较G
和H
快照,Git 将找到我们在提交H
中更改的内容。 通过使用G
中的元数据,Git 可以再往后退一步,提交F
。
当然,提交F
也有一个快照,并且有自己的元数据和另一个父哈希 ID。 通过向后跟踪每个提交,一次一跳,Git 可以回到任何人为此存储库所做的第一次提交。 该提交的特殊之处在于它没有父哈希 ID,这使得它成为 Git 所说的根提交。 Git 现在可以停止倒退,访问了导致提交H
的每个提交。这是存储库中的历史记录,至少对于上次提交为提交H
的分支而言。
Git需要提交H
的哈希 ID 来完成所有这些操作。 若要获取该哈希 ID,Git 可以使用你的分支名称。 例如,如果H
是分支master
的最后一次提交,我们可以像这样绘制它:
...--G--H <-- master
名称master
指向(定位)数据库中的提交H
;将H
点提交回G
;依此类推。 由于我绘制分支的方式,我此时停止绘制箭头:
...--G--H <-- master
I--J <-- develop
在这里,名称develop
指向J
;J
指向I
;I
指向H
. 通过并包括J
提交正在开发中,通过并包括H
提交master
。 这意味着许多提交都在两个分支上。 如果你习惯了其他版本控制系统,那么关于 Git 是一件奇怪的事情:提交在分支中出现和消失取决于你放置分支名称的位置。
重要的不是分支名称!是提交。 分支名称并非完全无关紧要,因为我们使用它们来查找提交,但它们大多无关紧要,因为我们可以在我们喜欢的所有内容周围更改它们。 例如,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
是branch1
上的提交,K
是branch2
上唯一的提交,L
是branch3
上唯一的提交。
这不是Git对待它们的方式。 通过H
的提交位于所有四个分支上,提交K
位于两个分支上。 其余三个提交都只是一个分支。 为了让 Git 和人类在这些事情上达成一致,我们最终要做的是使用以下形式的排除规则:
master..branch1
这实际上意味着:可从J
访问的所有提交的集合,减去可从H
访问的所有提交的集合。 这给了我们I-J
对。 同样,master..branch2
只给我们提交K
,branch2..branch3
只给我们提交L
。
分支名称不是唯一的名称类型
除了分支名称之外,Git 还可以通过许多其他类型的名称找到提交。 例如,在 GitHub 上,拉取请求会导致表单refs/pull/number/head
的名称出现在进行 PR 的存储库中。 在 GitHub 上,此特定名称链接到某个仓库中的某个分支名称 - 例如,您在分叉中的分支名称,或通过共享链接到同一仓库中的分支名称。
(Bitbucket使用的名称略有不同,但概念是匹配的。
您的情况
我们无法在各种存储库中看到各种名称,但我们可以近似地估计它们。 您自己有时只能看到其中一些名称,具体取决于许多事情(包括您是否进行了分叉以及托管站点的规则)。 但是我们知道,从您抱怨其他人的提交包含在您的 PR 中,PR 要求某些人添加提交的存储库中的实际情况看起来有点像这样:
...--G--H <-- master
I--J <-- someone-elses-work
K <-- your-PR
您所做的一个提交,提交K
,在其他人所做的两个提交之后:I-J
。 您的拉取请求要求他们(无论"他们"是谁)将您的提交K
合并到他们的分支master
中。 GitHub 将其称为拉取请求的基本分支。
由于无法更改提交,因此他们只能通过添加所有三个提交来添加提交K
。 因此,您的 PR 要求添加所有这些提交。
如何提出仅包含我的提交的干净 PR?
您必须进行新的提交。
[根据接受的 https://stackoverflow.com/q/14034718/1256452 答案],我必须创建一个新分支,从我的旧分支中挑选提交,并提出一个[新] pr
这些特定的步骤是不必要的,特别是Bitbucket不再需要"提出新的PR"(但早在2014年)。 然而,总体思路总体上是正确的。
假设我绘制的图表是准确的(或足够接近),提交K
就是问题所在。 它增加了提交J
。 相反,您需要一个添加到提交H
的提交。 要实现这一点,您必须进行新的和改进的提交 - 换句话说,一个樱桃选择。
不过,让我们在存储库中重新绘制分支图,如下所示:
...--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
然后挑选K
副本,我们可以称之为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 将:
像这样使用分离的 HEAD模式来提交
H
:...--G--H <-- HEAD, master I--J <-- branch-X K <-- feature
列出提交
K
的哈希 ID(使用内部1branch-X..feature
),挑选提交K
以获得K'
;最后强制名称
feature
指向K'
。
最终结果是:
K' <-- feature
/
...--G--H <-- master
I--J <-- branch-X
K [abandoned]
但是,您没有那么多的动作(例如,没有单独的git checkout
和git cherry-pick
操作),也没有不得不删除旧feature
然后重命名临时分支名称的轻微痛苦,以及丢失任何相关上游等相关的头痛。 因此,这绝对是一种更用户高效的方式来到达您想去的地方。
1实际上,git rebase
的内部结构比这复杂得多。 有岔路点魔法,--no-merges
部分,对称差异和樱桃标记/git cherry
上游等效省略。 但stop..start
的东西是从哪里开始的,对于这种情况,任何并发症都不应该成为障碍。 不过,这就是为什么教程应该从挑选而不是变基开始!
"基础分支"很重要
我不知道 Bitbucket 怎么称呼它,但是在引发 PR 时,"基本分支"是 GitHub 确定您实际要求合并的提交范围的方式(通过指定合并目标分支名称)。 GitHub 的机制过于复杂,他们拒绝显示实际的提交图,这使得这非常棘手。 您可以更改 PR 的基本分支;这样做的影响也很棘手:例如,我不清楚,如果你移动到后代提交,然后回到原始提交,甚至是它的祖先之一,会发生什么。