可以将git推送到远程仓库的当前分支吗?

3

来自 https://stackoverflow.com/a/34535193/156458

当你向远程仓库的检出分支推送时,通常会收到警告,Git 不允许这样做

但是我从《使用 Git 进行版本控制》(第二版)的 Loeliger 中得到了这样的暗示:Git 可以向远程仓库的当前分支推送,尤其是以下加粗的文本:

推送操作可以更新存储库状态,包括HEAD提交。也就是说,即使远程端的开发人员什么都没做,分支引用和HEAD可能会改变,与检出的文件和索引不同步。
在进行异步推送的存储库中积极工作的开发人员将看不到推送。但是,该开发人员随后进行的提交将在意外的HEAD上进行,从而创建奇怪的历史记录。强制推送将丢失其他开发人员推送的提交。该存储库的开发人员还可能发现自己无法将其历史记录与上游存储库或下游克隆相协调,因为它们不再是简单的快进。而且她不会知道原因:存储库已经在她的底下悄然更改了。猫和狗将生活在一起。这很糟糕。
因此,建议您只将内容推送到裸存储库中。这不是一个硬性规定,但对于普通开发人员来说,这是一个很好的指南,被认为是最佳实践。有些情况和用例需要您将内容推送到开发存储库中,但您应该充分了解其影响。当您确实想要将内容推送到开发存储库中时,您可能希望采用以下两种基本方法之一。
在第一个场景中,您确实希望在接收存储库中有一个带有检出分支的工作目录。例如,您可能知道没有其他开发人员会在那里进行活动开发,因此没有人可能被推送到他的存储库中的静默更改所盲目地击败。
在这种情况下,您可能希望在接收存储库中启用钩子,将一些分支(可能是刚刚推送的分支)检出到工作目录中。为了验证接收存储库在自动检出之前处于健全状态,钩子应确保非裸存储库的工作目录不包含编辑或修改的文件,并且当推送发生时,其索引没有处于暂存但未提交状态的文件。当这些条件不满足时,您有失去这些编辑或更改的风险,因为检出将覆盖它们。
还有另一种情况,将内容推送到非裸存储库中可以正常工作。根据协议,每个推送更改的开发人员必须推送到未检出的分支,该分支仅被视为接收分支。开发人员从不推送预期检出的分支。特定的某个开发人员负责管理检出的分支以及何时检出它们。也许该人负责处理接收分支并将它们合并到主分支中,然后再检出主分支。

这是否意味着git可以将内容推送到远程仓库的当前分支?(我猜是,但不确定)

上一个段落(建议使用挂钩检出由推送更新的分支)是否假定要推送的分支不是远程仓库中的当前分支?(我的想法是检出要推送的分支意味着要推送的分支不是当前分支,但最后一段指出了“不同的情况”,即推送到未检出的分支,这让我觉得前面的段落是关于将内容推送到已检出的分支,即当前分支)


1
可以进行这样的推送;由远程(通过该远程的配置条目)决定允许什么。因此,你引用的 SO 回答中有“通常”一词。(没有时间详细说明。) - torek
谢谢。在倒数第二段中,建议使用一个钩子来检查由推送更新的分支。它是否假定要推送到的分支是远程仓库中的当前分支?我猜不是。但如果是,这是否意味着检出一个当前的分支,即已检出的分支? - Tim
我不确定倒数第二段引用的内容是否是一个好主意。我曾经使用一种类似的设置,但它有一定的陷阱... - torek
3个回答

7
自 Git 2.3+ 版本开始,你可以配置接收端,使其“在接收库中拥有一个已检出分支的工作目录”。 特别地,{{link1:“push-to-checkout”(git 2.4,2015 年 5 月)}} 改进了 {{link2:“push-to-deploy”(Git 2.3,2015 年 2 月)}}。 请参考 "使用 Git push 部署项目" 以获得具体示例。
  • 首先:push-to-deploy(git 2.3):

你可以直接将更改推送到服务器上的存储库。在服务器上没有进行本地修改时,会自动检出服务器当前分支的任何更改。立即部署!

要使用此功能,必须首先在服务器上的 Git 存储库中启用它,方法是运行

$ git config receive.denyCurrentBranch updateInstead
  • 然后是 push-to-checkout 钩子(git 2.4)

现在有一个 push-to-checkout 钩子,可以安装在服务器上以自定义用户推送到已检出分支时发生的确切情况。
例如,默认情况下,如果服务器上的工作树有任何更改,则此类推送将失败。相反,push-to-checkout 钩子可以尝试将任何服务器端编辑与新分支内容合并,或者它可以无条件地用推送的分支内容的原始副本覆盖任何本地更改。

简而言之,您可以直接推送到已检出的分支。
请参见 commit 4d7a5ce 的注意事项:

  • 只针对工作树(working tree)的更改而不是索引(index)的更改仍然需要保护;

  • 在工作树中未被跟踪的文件如果会被 push-to-deploy 覆盖,则需要进行保护;

  • 使文件与要推送的内容相同的更改仍然需要保护(即该功能的清洁度要求比 checkout 更严格)。


3
我会尝试回答这个问题,但不想陷入引用书籍的细节。我认为应该从“接收方”的角度来看待这个问题,即考虑当普通用户使用git并且有人向他们的存储库推送时会发生什么(相对于通常所做的,即从别人那里获取)。Git提供了许多机制,并提供了一个可以正常工作的默认策略:“拒绝”。 :-) Git的新版本(2.3+)添加了几个更多的策略,既提供安全性,又允许一些推送;你是否认为它们是“正确”或“完美”的更多是一种观点问题。(考虑如果有人开始在您的非裸露的“部署”主机上进行编辑,然后,比如说,睡着了,因此没有其他人可以将其推送到它,因为现在它“看起来脏”了git。)
首先要记住,一个仓库可以设置为“裸”(bare),这告诉Git不要寻找工作树。 (你可以将非裸仓库转换为裸仓库,反之亦然,但根据我的经验,大多数人在此过程中都会出错。使用git clone --bare 最初设置裸克隆,并避免创建工作树,这意味着在这里没有混淆或错误的可能性.1)给定一个没有工作树的仓库,没有人进入其工作树并进行任何工作,“push打乱正在进行的工作”的情况是不可能发生的。
有了这个想法,让我们来看看当我们是推送接收者时会发生什么,并且我们有一个非裸仓库,其中有一个实际的工作树。 我们还需要记住另外两件事:
因为我们有一个工作树,所以我们也有一个索引文件 (.git/index),它在其通常的双重角色中起作用,即“下一次提交的暂存区”和“加速 git status 等操作的缓存区”。
我们还有一个“当前”分支,存储在 HEAD 文件中2,我们可以直接查看它,或者使用 git symbolic-ref HEAD 命令来读取(后者是正式批准的方法)。如果当前分支是 br,则 HEAD 文件包含一个单独的行,内容为 ref: refs/heads/br,例如,而 git symbolic-ref HEAD 命令则打印出 refs/heads/br
(如果我们处于“分离头指针”模式,则 HEAD 文件包含原始 SHA-1,而不是 ref: refs/heads/branch。在这种情况下,接收推送不会破坏正在进行的工作,因此我们可以安全地忽略此情况。)
以下是接收推送的基本机制:
  1. 作为接收方,我们首先接收要添加到我们仓库的对象。3 即使我们最终会拒绝一个或多个引用更新,我们也会添加所有这些对象。

  2. 现在我们得到了一个提案列表,其一般形式如下:“请将refs/heads/X设置为1234567...”,“强制将refs/heads/Y设置为fedcba9...”等。这些对应于启动他们的push的人使用的refspecs。(如果提供的SHA-1全是零,他们的git要求我们删除这些引用。)

    我们逐个考虑每个参考更新请求,部分按照规则进行,部分整体进行,应用下面列出的子规则。对于通过的更新,我们将提供的参考设置为提供的SHA-1,并告诉另一个git“好的,完成了”;对于失败的更新,我们告诉另一个git“不行,拒绝了”,并提供更多的“原因”信息。(我们还将钩子的stderr输出和有时的stdout输出传递给另一个git。有一个小协议告诉他哪些是我们自己的答案,哪些只是传递的输出,以便他的git知道我们接受了哪些更新。)

  3. 一旦我们全部完成,我们会运行一个“post-receive hook”,并将成功的更新(与pre-receive hook的形式相同,但消除了被拒绝的更新)传递给它。

现在让我们(轻松地)介绍一下用于接受或拒绝单个更新和/或整体更新的规则。这些规则不一定按照实际内部顺序排列(我将忽略一些特殊情况,例如receive.shallowUpdates)。
  • 一些更新必须通过各种内置测试,最常见的是“快进”测试,有时还会进行“永远不要更改此内容”测试。确切地说,哪些引用以及以哪种方式进行测试取决于我们使用的git版本、我们的配置以及此更新的force标志。有关详细信息,请参阅{{link1:git config文档}},特别注意receive.denyDeletesreceive.denyNonFastForwards,请注意git曾经将快进规则应用于标记更新(但不适用于标记删除),直到git 1.8.2,当标记被更改为“永远不要更改”(但仍允许删除,除非设置receive.denyDeletes)。

  • 整个更新集发送到pre-receive钩子中(作为标准输入的一系列行)。首先它们会附加一条更多的信息:与每个引用相关联的当前SHA-1或如果我们没有该引用,则为全零。如果该挂钩退出非零,则拒绝整个更新集——整个推送。(如果挂钩不存在,则认为此测试已通过。)

  • 每个单独的更新都会发送到update钩子中(作为参数)。如果该挂钩退出非零,则拒绝此特定更新,但我们会继续验证其他更新。(与之前一样,如果挂钩不存在,则测试将自动通过。)

  • 最后,我们有“非裸库”规则,这是您在此处关心的规则,并且我将将其单独分成一个部分。

更新非裸库:允许哪些操作以及原因

(我看到VonC已经完成了这个, 但我会提供更详细的内容.)

在git 1.6.6、git 2.3和git 2.4中,新的配置项或值有:

  • receive.denyDeleteCurrent: 这个选项实际上是在git 1.6.2中引入的,但直到git 1.6.6才真正起作用。在此之前,无论当前分支是什么,都会删除当前分支的引用。这样做时,HEAD指向不存在的分支(修复它需要使用plumbing工具或直接修改文件)。在git 1.6.6中,默认情况下不再允许这样做。(我没有测试“坏HEAD”是否仍然发生。)

  • receive.denyCurrentBranch: 这也是在1.6.2中引入的,并在1.6.6中启用(即默认的“拒绝”操作已生效)。但是,在git 2.3中增加了新的“updateInstead”值。

请注意,这两个命令都是针对“当前分支”而言的,即指代 HEAD 引用的单一参考 refs/heads/br。并且需要强调的是,它们仅适用于未设置 core.bare 的情况。在这种情况下,存在一个工作树,其中充满了与文件有关的内容,这些内容以某种方式与存储在 refs/heads/br 中的 SHA-1 相关联。此外,还可能存在(或者不存在)索引文件,该文件可能已经添加、删除,并且如果您处于冲突合并的中间状态,则可能保持合并状态。

假设通过 receive.denyCurrentBranch,您允许某人的 git push 更改您存储的 SHA-1 引用 refs/heads/br。进一步假设您没有设置任何部署钩子,并且不使用新的(2.3+)功能。那么,在这种情况下,如果其他人更改了您的 refs/heads/br,您自己的索引和工作树将完全不会受到影响。为了具体说明,假设 br 曾经指向提交 2222222...,现在其他人——比如 Bob——刚刚成功推送并将其更改为 3333333...

如果您现在完成了自己的编辑/合并/等操作,请像往常一样使用git add添加结果,然后运行git commit,Git将从您当前的索引中创建一个新的提交,其中包括“来自提交2222222...的所有内容,但不包括您的git addgit rm”。Bob做的事情,即在3333333...中的更改,不在您的索引中。Git制作的新提交将以3333333...为其父级,同时使用基于2222222...的索引中获取的内容。其效果是,您的提交会撤销Bob的所有更改,同时添加您自己的更改:将您的新提交与2222222...进行比较,您可以看到您做了什么,将您的新提交与其父级进行比较,您可以看到撤消了Bob的所有工作,同时保留了您自己的工作。
如果您确实有一个执行部署操作的钩子,那么索引和/或工作树的内容将取决于该钩子的具体操作。例如,如果它执行了“git checkout -f”,那么Bob更改的所有内容都将替换您放入索引和工作树中的内容。这两种结果都不是任何人真正想要的。
新的“updateInstead”设置更接近人们有时想要的:在允许引用更新(Bob将“refs/heads/br”从“2222222...”更改为“3333333...”)之前,Git会检查您的索引和工作树是否与提交“2222222...”匹配。如果匹配5,Git将允许Bob的推送并将该更新应用于您的索引和工作树,就像您已经发现了Bob的推送并执行了“git checkout br”或任何等效操作以使一切保持最新一样。
这里仍然存在一些潜在的危险。例如,假设您已经打开了README进行编辑。您花了一些时间查找一些参考URL并在编辑器中输入它们,但没有将结果写入任何地方。同时,Bob修复了README并运行了他的git push。您的git看到您的工作目录是“干净的”,更新是“安全的”,因此更新了您的README
根据您的编辑器的聪明程度,当您要写出您的README时,您可能会覆盖Bob的更改,或者您的编辑器可能会说“嘿,README已更改,我会获取新的”,并且丢失您的工作等等。有人可能会认为这是那个编辑器的不良行为(我会接受这个观点),但这仍然是一个潜在的问题 - 并且不限于编辑器;您可能正在运行一些缓慢的计算过程,编写您保留源代码控制的文件,这可能会产生相同类型的问题。
Git不试图决定如何处理所有这些。Git只提供配置选项(更多机制),并将最终策略留给您。我会说git在这里的默认值是正确的;更高级的updateInstead模式不是默认值,因为“正确”的策略不清楚。

1还有其他可能的错误,这取决于您是否想要简单的ssh推送的组写模式和共享。在以前的工作场所,我们最终制定了一个策略,使用脚本配置可推送的存储库:您将想要在其中看到的内容设置为私有存储库,然后自己运行脚本或让管理员运行它,并给它一个URL来创建公共共享存储库的克隆。之后,我们不关心您对该私有存储库做了什么,但主要问题在于我们会使用--bare进行克隆,而不是必须让某人(通常是我)去修复所有损坏的部分。 :-)

2即使是裸存储库也有一个HEAD文件,因此具有当前分支。它还有一个索引,但由于没有工作目录,索引通常是无关紧要的。(一些部署脚本最终使用裸存储库的索引,这导致其中一些部署脚本出现错误,但这是另一个完全不同的问题。)当前分支稍微相关:它影响其他人的git clone在克隆过程结束时为他们检查出哪个分支,前提是他们没有指定特定的分支名称。

通常我们会将它们作为“薄包”获取,也就是说,这种包可以与我们已经拥有的对象进行增量压缩。为了实现这一点,在“接收对象”步骤之前,有一个步骤,我们告诉发送方我们拥有哪些SHA-1码。您可以通过在发送方使用git ls-remote来查看我们向发送方发送的内容。还有一些早期的协议协商步骤。这些对于低级细节很重要,但对于上述过程并不重要。
您可以删除.git/index,当git需要它时,git会重新构建它。我不特别建议删除它,但效果是失去所有存储的git addgit rm,以及如果您处于合并过程中,则失去所有合并信息。
并且其他附加测试通过(参见VonC's answer)。其中一些附加测试不在最初尝试的updateInstead模式中,我认为是通过艰难的方式发现的。

像往常一样详细而富有启发性。+1 - VonC

2
这句话的意思是:Git是否可以将代码推送到远程仓库的当前分支?(我猜是可以,但不确定)
答案取决于您使用的 Git 版本。在 2 版本之前,您必须明确告诉 Git 推送到哪个分支,但从 2.X 版本开始,已经改变了。
所有细节都可以在这里找到。

Git v2.0 发行说明

向后兼容性说明

git push [$there]没有指定要推送的内容时,我们一直使用传统的“匹配”语义(只要远程仓库中已经有相同名称的分支,所有你的分支都会被发送到远程仓库)。

在 Git 2.0 中,默认现在是“简单”语义,它会将:

  • 仅当前分支推送到具有相同名称的分支上,并且仅当当前分支设置为与该远程分支集成时,如果您正在从相同的远程仓库获取;或者

  • 仅当前分支推送到具有相同名称的分支上,如果您正在推送到不是您通常获取的远程仓库。

您可以使用配置变量“push.default”来更改此设置。如果您是一个想继续使用“匹配”语义的老手,您可以将变量设置为“matching”,例如。阅读其他可能性的文档。


设置上游分支(给定分支的远程分支)

您可以为每个分支设置默认的(获取)远程分支,该分支将“通信”(拉取/推送)。

这些设置在每个分支的.git/config文件中定义。
您也可以使用以下命令手动更改它:

git branch --set-upstream-to=upstream/foo

1
他在询问接收方会发生什么,而不是发送方。 - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接