将远程提交应用于本地拉取请求



我已经将GitHub上的一个活跃的开源项目分叉到我的帐户。然后,我将我的 GH 存储库克隆到我的本地计算机。我定期获取上游主分支以保持我的本地副本同步。

我对上游存储库上的一项重要新功能的拉取请求感兴趣,该功能目前正在审查和更改中。我想将拉取请求复制到我的本地计算机,以便我可以查看和分析此代码。我不会提交对这项工作的任何更改。

我用这个命令和结果获取了拉取请求:

$ git fetch upstream pull/5737/head:pr-5737 
remote: Counting objects: 4, done. 
remote: Total 4 (delta 3), reused 3 (delta 3), pack-reused 1 
Unpacking objects: 100% (4/4), done. 
From https://github.com/OrigProj/repo-name 
* [new ref]     refs/pull/5737/head -> pr-5737

结果是一个名为"pr-5737"的新分支用于拉取请求。 项目开发人员现在在处理拉取请求时对拉取请求进行了额外的提交。我可以使用以下命令将拉取请求的新提交下放到我的本地机器:

$ git checkout pr-5737 
Switched to branch 'pr-5737' 
$ git fetch upstream pull/5737/head
remote: Counting objects: 4, done. 
remote: Total 4 (delta 3), reused 3 (delta 3), pack-reused 1 
Unpacking objects: 100% (4/4), done. 
From https://github.com/OrigProj/repo-name
* branch         refs/pull/5737/head -> FETCH_HEAD

我无法弄清楚将这些新获取的提交"合并"到拉取请求分支的头部的命令。我不想将拉取请求合并到主分支中。我想保持单独的分支 pr-5737 更新。我使用什么命令?

我知道我可以删除拉取请求分支 pr-5737 并重新获取它,但这似乎不是"正确"的做事方式。

答:

从托雷克的出色解释/答案中挑选樱桃。并根据我目前的实践采取简单的道路。我能够将新的提交从上游添加到拉取请求分支 pr-5737。

我使用了以下命令:

$ git checkout master
$ git branch -f pr-5737 FETCH_HEAD

然后我把所有东西都推送到上游项目的GitHub分支上。主要是出于备份的原因。您还将看到我添加了我想要的另外两个拉取请求分支。

$ git push  -v --all origin
Pushing to git@github.com:username/fork-project.git
Enter passphrase for key '/home/acct-name/.ssh/id_ecdsa':                                                                                                                   
Counting objects: 68, done.                                                                                                                                             
Delta compression using up to 4 threads.                                                                                                                                
Compressing objects: 100% (68/68), done.                                                                                                                                
Writing objects: 100% (68/68), 11.38 KiB | 0 bytes/s, done.                                                                                                             
Total 68 (delta 50), reused 0 (delta 0)                                                                                                                                 
remote: Resolving deltas: 100% (50/50), completed with 21 local objects.
To git@github.com:username/fork-project.git                                                                                                                                
= [up to date]      pr-5717 -> pr-5717                                                                                                                                 
ec0b073..67486dc  master -> master                                                                                                                                   
212c54a..e935946  pr-5737 -> pr-5737                                                                                                                                 
* [new branch]      pr-5763 -> pr-5763                                                                                                                                 
updating local tracking ref 'refs/remotes/origin/master'                                                                                                                
updating local tracking ref 'refs/remotes/origin/pr-5717'                                                                                                               
updating local tracking ref 'refs/remotes/origin/pr-5737'                                                                                                               
updating local tracking ref 'refs/remotes/origin/pr-5763'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

你正在做的事情不是"内置"到 Git 中,所以不一定有正确的方法。 只有很多选择。 有一种方法 - 或者多种方法,实际上 - 来处理这个问题是内置的,所以你可能想切换到它,但让我们从你正在做的事情开始。

让我们先看看运行git fetch时会发生什么。 我在这里假设您有两个遥控器,一个名为origin,另一个名为upstream。 以下是这些远程名称为您执行的操作:

  • 每个人都记得一个网址。

    origin的 URL 和upstream的 URL 是不同的,甚至不需要都在 GitHub 上(尽管既然你提到了 GitHub 分叉,我假设两者都是)。 不过,您可以输入一个较短的名称。

  • 每个分支还为远程跟踪分支名称提供前缀。 例如,您现在同时拥有origin/masterupstream/master

这是第二部分,您可以(用管理术语)"利用"拉取请求。 但首先,让我们谈谈分支、分支名称和命名空间。

Git 分支与 Git 分支名称

Git 中的术语分支是模棱两可的。 它可以表示分支名称,如masterpr-5737,但它也可以指提交图中的分支结构。 每个提交都有自己唯一的哈希 ID,每个提交还会记录上一个提交的 ID。 例如,在一个只有三个提交的新仓库中,我们可能有:

A <- B <- C   <-- master

其中名称master包含提交C的 ID,C本身包含提交B的 ID。 我们说master指向CC指向BB指向A。 由于A是第一次提交,因此它没有什么可以指向的;我们称之为提交。 这些指针都是向后工作的 - 分支名称为我们(和 Git)查找提示提交,而提示提交查找较早的提交,该提交查找更多提交,一直返回到根目录。

添加新提交仅意味着编写指向当前提示的提交,然后更新分支名称以指向新提交:

A <- B <- C <- D   <-- master

我通常绘制这些没有内部向后箭头,这使得绘图更加紧凑并允许显示分支:

A--B--C--D   <-- master

E--F    <-- feature

再次注意,分支名称仅指向(在本例中为两个)提示提交。 正是这些提交指向其余提交。 这些提交链也是 Git 分支。 有关此内容的更多信息,请参阅"分支"到底是什么意思? 在 Git 中,我们互换使用术语分支来表示提交图中的分支名称和分支结构。 通常很明显哪一个的意思 - 但如果不是,你应该问!

这里的一个关键项是,提交可以通过名称而不是分支名称的东西来指向。 例如,标签也可以指向提交(当然提交指向提交)。 对这些名称的限制很少,事实上,拉取请求只是指向提交的另一类名称。 所有这些的通用术语是引用:在 Git 中,引用是提交的任何名称——通常是分支(图分支)的提示提交——或任何其他 Git 对象(但我们在这里忽略其他三种类型的 Git 对象)。1

另一个关键项是,在正常(非维护命令)使用中,Git只能使用这些名称在提交图中查找提交。 它需要一个分支名称、标签名称或其他东西来查找起始 ID。 (你可以给你的 Git 一个原始 ID,可能是缩写的,例如,如果你运行git log ac0ffee,这就是你所做的。 它从该提交开始,然后从那里向后工作。


1Git 的某些部分断言所有引用都以refs/开头,但其他部分指出存在特殊的非refs/引用,例如HEADORIG_HEADCHERRY_PICK_HEADMERGE_HEAD等。 我会说,因为大多数参考资料都以refs/开头。


git fetch引入提交

当你运行git fetch时,你会让你的 Git 调用另一个 Git。 另一个 Git 有自己的存储库。 你的 Git 使用存储在远程名称下的 URL 联系其他 Git,然后你的 Git 和他们的 Git 进行一些对话。 他们的 Git 会告诉你的 Git 他们的分支提示(名称和提交(哈希 ID),由这些名称标识。

这些哈希 ID 以复杂(加密)但完全确定的方式从提交的内容计算。 任何两个 100% 相同的提交,逐位,具有相同的ID。 这意味着您的 Git 及其 Git 在两个存储库中的任何提交都具有相同的 ID。 因此,您的 Git 可以判断您是否已经有他们的提交。

如果你的 Git 没有他们的提交,并且你已经告诉你的 Git 引入它们(通过请求该名称或所有名称),则你的 Git 会从他们的 Git 请求这些提交。 他们的 Git 将它们捆绑并发送过来,您的 Git 使用它们唯一的哈希 ID 将它们存储在您的存储库中。

提取现在必须分配名称

Git 现在需要一些方法来找到这些提交,现在它这些提交。 这意味着git fetch必须保存一些名称。 当然,你的 Git 从其他 Git 获得的所有分支提示和其他提示提交都在那里有名称。 为什么它不能只使用这些名称?

想一想,原因就很明显了。 假设它获得的名称是"分支主",并且它用这个新值覆盖了您的master。 您将失去对自己的提交的轻松访问!阿拉伯数字

git fetch保存这些名称/ID 对的最简单方法是将它们全部写入名为FETCH_HEAD的文件中。 他们将安然无恙地呆在那里,直到下一个git fetch覆盖它们。

这就是你用这个(第二个)命令所做的:

$ git fetch upstream pull/5737/head
remote: Counting objects: 4, done. 
remote: Total 4 (delta 3), reused 3 (delta 3), pack-reused 1 
Unpacking objects: 100% (4/4), done. 
From https://github.com/OrigProj/repo-name
* branch         refs/pull/5737/head -> FETCH_HEAD 

remote:消息来自另一个 Git:它被打包成一个对象(可能是一个提交、一个树和两个 blob,尽管我只是猜测)打包到一个应用了增量压缩的精简包中("delta 3")。 你的 Git 得到了包并解压缩了它。 你的 Git 使用了他们的 Git 中的一个名字——refs/pull/5737/head——就像你告诉它的那样。 而且,您的 Git 没有将其存储在您自己的名下,而只是存储在FETCH_HEAD文件中。

如果您愿意,您现在可以从文件FETCH_HEAD中提取此提交的 ID。 您可以通过查看文件内部(格式相当明显)来执行此操作,或者(因为其中只有一个 ID)只需使用名称FETCH_HEAD. 请记住,下一次获取将覆盖文件,忘记 ID。 一旦这种遗忘发生,如果你刚刚得到的新提交没有可以找到它的名字,这使得提交有资格进行垃圾回收:它最终会被丢弃。 但是你现在有机会给它起你自己的名字。

不过,让我们与您的第一个命令进行比较:

$ git fetch upstream pull/5737/head:pr-5737

请注意此处的冒号,输出以:

* [new ref]     refs/pull/5737/head -> pr-5737

前面的命令没有说FETCH_HEAD. 相反,您的 Git 将名称写入了一个新的引用,pr-5737. 这个参照实际上是refs/heads/pr-5737的,通过git fetch所做的一些假设。3现在,我们只注意任何分支的"全名"都是refs/heads/branch的,例如,分支名称masterrefs/heads/master

顺便说一下,这种冒号分隔的形式是一个refspec。 refspec 仅略多于或小于一对分支名称:源名称和目标。 对于git fetch,源是他们的名字(分支或其他引用),目的地是您的名字。 您可以为任何一方选择任何类型的名称,而不仅仅是分支名称。 对于fetch,省略这样的目标名称意味着您只想将信息写入FETCH_HEAD


2但请注意,这是(部分)标签的工作方式。 标签的想法是跨所有存储库克隆是全局的。 那么问题就变成了你自己的标签是否以及何时会被另一个 Git 的标签覆盖,答案很复杂(不适合这篇文章)。

3git fetchgit push都有一些复杂的代码来限定非限定引用。 像masterbranchpr-5737这样的名称不以refs/开头,因此它是不合格的。 如果你写出refs/pull/5737/head或类似的,这确实refs/开始并且是限定的,并且不会经过这个复杂的代码。 在少数情况下,Git 无法自己进行限定,或者做错了,并让你写出全名。 例如,对于这些拉取名称也是如此。 不过,通常情况下,它在猜测某物是分支还是标签方面做得很好。 在这种情况下,它会猜测您打算创建一个分支,这可能是正确的。

4从 Git 版本 1.8.2 开始,Git 将对远程跟踪分支进行机会性更新。 我们将在本文的最后看到有关此内容的更多信息。


快进和强制更新

我无法弄清楚将这些新获取的提交"合并"到拉取请求分支的头部的命令。

是时候回到图形图了。

您的第一个git fetch带来了一些提交 - 可能只有一个,再次 - 但它在您的存储库中为它们提供了一个分支名称,pr-5737。 该提交本身指向以前的提交,我现在猜测该提交主要或仅在其(upstream)分支master上。 因此,图形片段为:

...--o     <-- upstream/master

o   <-- pr-5737

现在,可能是您已经更新了自己的master以匹配upstream/master。 在这种情况下,我们应该这样画图:

...--o     <-- master, upstream/master

o   <-- pr-5737

请注意,提交图保持不变!我们所做的只是稍微改变了标签。这是这个过程的关键。 获取将始终为您提供提交;您要做的是更改(或添加)标签,例如分支名称,因为您可以将这些提交合并到您自己的图形中。fetch步骤修改(添加)提交图,之后,我们需要对名称进行一些处理。

那么,让我们看一下第二个git fetch之后的图表。 首先,假设这又增加了一个指向上一个拉取请求提交的提交:

...--o       <-- upstream/master

o     <-- pr-5737

o   <-- FETCH_HEAD

在这种情况下,您可能希望移动pr-5737以指向与FETCH_HEAD相同的提交。 但是,如果新提交没有指向以前的拉取请求提交怎么办? 相反,如果它指向origin/master提交怎么办? 好吧,让我们画出来:

...--o     <-- upstream/master
|
| o   <-- pr-5737
|

o   <-- FETCH_HEAD

现在您可能希望移动pr-5737以指向新的提交。 (或者,也许你不想这样:由来决定你想要什么。 但我假设你确实想要它。

有一堆 Git 命令可以移动分支标签。 最面向用户的是git branch:使用git branch --force您可以重新设置当前未签出的任何分支。 (要重新设置您已签出的分支,您需要改用git reset,出于一系列很好的技术原因,相当于 Git 让实现显示出来。 你可以运行:

git branch -f pr-5737 FETCH_HEAD

强行移动名称pr-5737指向刚刚引入的提交git fetch

(同样,如果您目前pr-5737签出,则必须改用git reset,然后您必须选择:--soft--mixed--hard? 它们控制重置操作是否影响索引和工作树。 让我们假设您没有检查它。:-) )

现在,如果我们刚刚引入的新提交,即FETCH_HEAD处的提交,"添加到分支"——即,新的提交指向pr-5737的尖端——这个分支标签移动就是 Git 所说的快进。 请注意,在上面的第一个FETCH_HEAD图中,Git 如何将标签向前滑动(向右,同时也向下)到新提交:

...--o       <-- upstream/master

o

o   <-- pr-5737, FETCH_HEAD

但是,对于第二个绘图,强制pr-5737指向新提交会导致忘记旧提交! 我们必须将标签向上备份一步,指向upstream/master的尖端,然后向下和向右返回。 这是一个非快进的强制更新。

如果我们使用git branch -f,即使无法快进,它也会更新pr-5737。 如果您只想pr-5737快进时

移动怎么办?

合并也可以快进

毫无疑问,您在合并时看到了 Git 打印"快进"。 这是因为如果你在其中一个分支上,并运行git mergename,Git 将检查它在给定name(通常是分支的尖端)下找到的提交是否"快进">当前分支的提示提交。 如果是这样,Git 实际上并没有合并任何内容,它只是向前滑动分支名称(并签出新的提交,以便您的索引和工作树与新的分支提示匹配)。

如果使用git merge命令,则可以将其限制仅在合并确实是快进时才工作,使用--ff-only。 (而且,您可以强制它根本不执行快进,而是使用--no-ff进行新的合并提交。 因此,您可以git checkout pr-5737然后git merge --ff-only FETCH_HEAD实现快进。 当然,这可能会失败,就像第二种情况一样。 然后你必须决定你想做什么。

您可能不想合并这两个提交。 (如果你真的这样做,无论出于何种原因,你都可以:只需运行git merge FETCH_HEAD。 不过,这可能没有。 您可能只想强制分支移动并将新的提示提交加载到索引和工作树中,在这种情况下,您可以git reset --hard FETCH_HEAD. 但是,如果您采用这种方式,您将知道 - 基于git merge --ff-only是否有效 - 更新的拉取请求是替代品还是附加组件。

以简单的方式完成:git fetch可以为您完成

在你的原始git fetch中,你告诉你的 Git 写下这个名字pr-5737

$ git fetch upstream pull/5737/head:pr-5737

您可以再次使用此完全相同的命令。 您的 Git 将获得任何新提交,然后尝试更新您现有的refs/heads/pr-5737

和以前一样,这可能是一个快进。 在这种情况下,您的 Git 将进行更新。 (您仍然可以获得FETCH_HEAD文件,但不再需要它。 或者,它可能是非快进。 在这种情况下,您的 Git 将出错:

! [rejected]   refs/heads/pr-5737 -> pr-5737 (non-fast-forward)

为了强制更新,我们使用了 refspec 的另一个功能:它可以以加号开头,意思是">强制"。 所以:

git fetch upstream +pull/5737/head:pr-5737

这一次,您将获得更新,并添加了注释(forced update)。 仅当实际强制更新时,才会添加注释,因此与执行手动git merge --ff-only一样,您将知道是否必须强制更新。

真正简单、全自动的方式

现在,如果您可以让git fetch执行此更新而无需键入以下内容,那可能会很好:

git fetch upstream +pull/5737/head:pr-5737

一直以来。 事实上,您可以引入任何和所有具有表单refs/pull/NNNN/head的拉取请求,或者您想要识别的任何其他形式。 由您决定如何引入它们,但在我们深入研究该机制之前,让我们提及命名空间以及远程名称在远程跟踪分支名称中的作用。

名称空间(或作为单个单词的命名空间)是一个组织,通常是分层的,其中不同的名称组被分组。 例如,在 Git 的情况下,大多数引用都在refs/下,但所有分支名称都在refs/heads/下,正如我们之前看到的。 所有标签都在refs/tags/下。 这些名称的工作方式类似于目录(在某些情况下,实际上是这样实现的)。 以refs/remotes/开头的空间包含所有远程跟踪分支名称,但它进一步细分:在refs/remotes/origin/下,有一个名为origin的远程空格,在refs/remotes/upstream/下为upstream另一个空格。

通过细分远程跟踪分支,并将它们与常规分支分开,Git 保证它永远不会使用你自己的任何分支名称作为远程跟踪分支名称。 你自己的分支都以refs/heads/开头,refs/remotes/origin/不以refs/heads/开头,所以这些名称是分开的。 此外,通过包含遥控器的名称,Git 试图保证这些也永远不会发生冲突:refs/remotes/origin/总是与refs/remotes/upstream不同。5

如果您允许pr-number形式的拉取请求占用与分支相同的命名空间,并且如果您命名自己的分支之一,例如pr-123,则可能会发生冲突。 所以不要这样做:要么确保你永远不会像这样命名你的分支,要么为你的拉取请求跟踪器选择你自己的命名空间。 您可能希望坚持使用分支名称,因为 Git 只有三个内置表单可以识别,分别用于分支、标签和远程跟踪分支;因此,分支名称的键入时间较短。 (这就是为什么你必须拼出pull/5737/head而不仅仅是5737/head:全名是refs/pull/5737/head,Git 可以在refs下找到它,但不能没有pull部分。 您master的全名是refs/heads/master,但您不必输入heads/master

(出于我稍后会提到的原因,您可能希望将专用的拉取请求子分支名称空间拼写为pr/*而不是pr-*。 我假设从现在开始,你想要pr/5737而不是pr-5737

如果你在编辑器中打开你的.git/config文件——请注意,你可以用git config --edit来做到这一点,所以 Git 鼓励这样做;只要你的编辑器不尝试将配置转换为富文本或同样愚蠢的东西,它就相对安全——你会看到每个远程的配置部分:

[remote "origin"]
url = ...
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "upstream"]
url = ...
fetch = +refs/heads/*:refs/remotes/upstream/*

url行提供保存的 URL - 它们是您的 Git 知道如何调用其他 Git的方式。 对我们来说,更有趣的线条是fetch行。 这些提供默认的获取引用规范。

如果你运行git fetch origin这意味着

git fetch origin +refs/heads/*:refs/remotes/origin/*

这就是 Git 实现远程跟踪分支的方式git fetch只是获取您提供给它的 refspec,即使这些是每个远程fetch默认配置所暗示的那些。 Git 还允许多个fetch =行,并将所有这些行添加为默认的 refspec 集。

这表明 refspecs 也可以进行一种通配符匹配。 这种匹配是壳式形匹配的有限形式。 这意味着您可以添加第二行fetch =内容如下:

+refs/pull/*/head:refs/heads/pr/*

现在,如果远程在你运行git fetch时有一个名为refs/pull/5737/head的引用,你的Git 将创建或更新你自己的分支pr/5737

如果你的 Git 足够新,你可以在任何地方使用一个*glob,例如,相当奇特的:

+refs/pull/5*/head:refs/heads/pr-5*

这将仅获取以5开头的拉取请求,更新您自己的分支名称以pr-5开头。 但是在 2.6.0 之前的 Git 版本中(提交 cd377f4),*必须匹配整个组件,例如,pull/*/head但不匹配pull/5*/head,或者pr/*但不pr-*。 如果你的 Git 是 2.6.0 或更高版本,你可以提取到pr-*,但如果不是,你必须提取到pr/*

(从 Git 版本 1.8.2 开始,如果 Git 正在获取引用,并且它具有这些匹配的fetch =行之一,则 Git 将自动更新相应的远程跟踪分支,即使您在命令行上提供了一些 refspec。 这在旧版本的 Git 中不会发生。 但即使在那些旧版本的 Git 中,如果你成功地推送了与这些 refspec 之一匹配的内容git push,Git 也会机会性地更新远程跟踪分支。 推送这样做的事实最终说服了 Git 人员,fetch 也应该这样做。


5如果您命名一个远程a和另一个a/b,则此尝试会以微妙的方式失败。 Git 应该禁止远程名称中的斜杠,但它没有。 (因此,不要在远程名称中使用斜杠,或者如果这样做,请确保没有一个是另一个名称的前缀。


还有更多选择

您不必将拉取请求放入(常规)分支中。 您可以将它们添加为远程跟踪分支,例如,使用自己发明的pr/子命名空间:

[remote "upstream"]
+refs/pull/*:+refs/remotes/pr/upstream/*

这会将他们的pull/5737/head变成您的pr/upstream/5737/head远程跟踪分支。 现在,您可以选择是否git checkout 5737/head创建自己的名为5737/head的本地分支,该分支具有远程跟踪分支pr/upstream/5737/head(这是针对名为pr/upstream的"远程",您没有,但这没关系;尽管您必须确保不要将新的远程"pr"命名为上游。 (也就是说,5737/head@{u}将命名pr/upstream/5737/head

明显的缺点是名称有点笨拙。 不太明显的是,如果从多个远程收集拉取请求,如果两个远程都有未完成的拉取请求 #5737,则5737/head可能同时匹配pr/upstream/5737/headpr/another/5737/head远程跟踪分支。 在这种情况下,知道如何基于远程跟踪分支创建本地分支的 DWIMgit checkout功能将失败:它不会为您任意选择一个分支。

这也没有明显的优势。 您拥有自己的分支,因此您可以进行自己的提交 -但为什么要这样做呢? 使用没有远程跟踪分支的早期方案强制提取到您自己的分支空间的缺点是,如果您忘记pr/5737是以这种方式设置的并在那里进行一些您想要保留的提交,您可能会破坏自己的提交。 (但即便如此,默认情况下,您的pr/5737reflog 将保留您的提交 30 天。

因此,我不确定你为什么会想要这个——但这是一种选择。fetch = ...机制就是这样:一种机制,而不是一项政策。 这取决于你如何使用它。

最新更新