“git fetch --tags --force”和“git pull <branch>”是可交换操作吗?

10

通常,git标签是对提交的固定引用。但有时它们被用来标记某些事件(例如last-buildbase-line等),并且会经常更改。

我有一个脚本,可以从参考库中刷新这些“浮动”的标签。

git fetch --tags --force

还可以从一个分支进行拉取:

git pull origin <mybranch>

我知道许多Git用户警告不要使用浮动标签,但我被迫处理它。我的问题是:
如果分支由其中一个浮动标记标记...那么命令的执行顺序是否重要?
我担心git pull在本地存在标签时不会刷新它们,并且如果它先运行,则可能与所有标签的引用一起工作。 git pull有一个--force选项,但--no-tags选项的帮助部分解释了默认行为:
默认情况下,指向从远程存储库下载的对象的标记会被获取并存储在本地。
这是否意味着必须首先下载对象才能刷新标记?在这种情况下,git pull应该首先运行。
哪个是正确的顺序?

认为我知道你所说的“浮动标签”的意思,但在这里定义一下似乎会更好。 - torek
完成。感谢您的快速回复。 - yucer
3个回答

26
这涉及到Git中比较晦涩的角落,但最终答案是“最初使用哪种顺序并不重要”。然而,我建议一般情况下避免使用git pull,并且在脚本中永远不要使用它。此外,确切地说,何时获取数据是很重要的,我们将在下面看到。因此,我建议先运行自己的git fetch,然后根本不使用git pull

git fetch

普通的git fetch(没有--tags)默认使用奇怪的混合标签更新,尽管每个远程仓库可以定义一个默认的标签选项来覆盖此默认值。你引用的就是这个奇怪的混合标签:指向从远程仓库下载的对象的标签被获取并存储在本地。其基础机制有点棘手,我会在后面讲到。

--tags添加到git fetch参数中,几乎与在命令行上指定refs/tags/*:refs/tags/*具有相同的效果。(稍后我们将看到区别。)请注意,这不在refspec中设置force标志,然而测试显示获取的标签仍会被强制更新。
添加--force的效果与在每个显式refspec中设置force标志相同。换句话说,git fetch --tags --force大致相当于运行git fetch '+refs/tags/*:refs/tags/*':如果远程有标记refs/tags/foo指向提交1234567...,则您的Git将替换任何现有的refs/tags/foo,使您现在也有自己的refs/tags/foo指向提交1234567...。(但是实际观察到,即使只有--tags,它也会这样做。)
请注意,在所有情况下,git fetch都会将有关其获取内容的信息写入文件FETCH_HEAD。例如:
$ cat .git/FETCH_HEAD
e05806da9ec4aff8adfed142ab2a2b3b02e33c8c        branch 'master' of git://git.kernel.org/pub/scm/git/git
a274e0a036ea886a31f8b216564ab1b4a3142f6c    not-for-merge   branch 'maint' of git://git.kernel.org/pub/scm/git/git
c69c2f50cfc0dcd4bcd014c7fd56e344a7c5522f    not-for-merge   branch 'next' of git://git.kernel.org/pub/scm/git/git
4e24a51e4d5c19f3fb16d09634811f5c26922c01    not-for-merge   branch 'pu' of git://git.kernel.org/pub/scm/git/git
2135c1c06eeb728901f96ac403a8af10e6145065    not-for-merge   branch 'todo' of git://git.kernel.org/pub/scm/git/git

(之前的提取运行没有使用--tags,然后):

$ git fetch --tags
[fetch messages]
$ cat .git/FETCH_HEAD
cat .git/FETCH_HEAD 
d7dffce1cebde29a0c4b309a79e4345450bf352a        branch 'master' of git://git.kernel.org/pub/scm/git/git
a274e0a036ea886a31f8b216564ab1b4a3142f6c    not-for-merge   branch 'maint' of git://git.kernel.org/pub/scm/git/git
8553c6e5137d7fde1cda49817bcc035d3ce35aeb    not-for-merge   branch 'next' of git://git.kernel.org/pub/scm/git/git
31148811db6039be66eb3d6cbd84af067e0f0e13    not-for-merge   branch 'pu' of git://git.kernel.org/pub/scm/git/git
aa3afa0b4ab4f07e6b36f0712fd58229735afddc    not-for-merge   branch 'todo' of git://git.kernel.org/pub/scm/git/git
d5aef6e4d58cfe1549adef5b436f3ace984e8c86    not-for-merge   tag 'gitgui-0.10.0' of git://git.kernel.org/pub/scm/git/git
[much more, snipped]

我们稍后会回到这个问题。
取回操作可能会根据找到的其他引用规范(通常由remote.origin.fetch配置条目控制)更新一些远程跟踪分支,并创建或更新一些标签。如果您配置为提取镜像,并且其更新引用规范为+refs/*:refs/*,则会获取所有内容。请注意,此引用规范设置了force标志,并带有所有分支、所有标签、所有远程跟踪分支和所有注释。关于使用哪些引用规范的更加晦涩的细节,但使用--tags选项,无论是否使用--force选项,都不会覆盖配置条目(而明确编写一组引用规范会覆盖配置条目,因此这是--tags与编写refs/tags/*:refs/tags/*之间可能唯一的区别)。
在你自己的引用空间中进行的更新——通常是你自己的远程跟踪分支和标签——很重要,但...对于pull命令来说却不是,我们将在下一节中看到。
git pull
我想说,git pull 实际上只是运行了两个 Git 命令:首先是 git fetch,然后是第二个 Git 命令,默认情况下为 git merge,除非你指定使用 git rebase。这是正确的,但有一个不太明显的细节需要注意。在 git fetch 被重写为 C 代码之前,这样说起来更容易理解:当它还是一个脚本时,你可以跟随脚本中的 git fetchgit merge 命令,查看实际参数是什么。
git pull 运行 git mergegit rebase 时,它不会使用你的远程跟踪分支和标签。相反,它使用 FETCH_HEAD 中留下的记录。
如果您仔细观察上面的例子,您会发现它们告诉我们最初在git.kernel.org上的存储库中,refs/heads/master指向提交e05806d...。在我运行了git fetch --tags之后,新的FETCH_HEAD文件告诉我们,在我运行fetch时(现在可能已经改变),在git.kernel.org上的存储库中,refs/heads/master指向提交d7dffce...
git pull运行git mergegit rebase时,它会将这些原始SHA-1数字传递过去。因此,您的引用名称解析为什么并不重要。我运行的git fetch实际上确实更新了origin/master
$ git rev-parse origin/master
d7dffce1cebde29a0c4b309a79e4345450bf352a

即使没有这个问题,git pull 命令也会将 d7dffce1cebde29a0c4b309a79e4345450bf352a 传递给第二个命令。

因此,假设您在没有使用 --force 的情况下获取标签,并获得了对象 1234567...。进一步假设,如果您使用了 --force 来获取标签,那么这将是 git rev-parse refs/tags/last-build 命令的结果。但是,由于您没有使用 --force,您自己的存储库将 last-build 指向 8888888...(这是中国非常幸运的提交 :-))。如果您个人说“告诉我关于 last-build”,您将获得修订版 8888888...。但是,git pull 知道它获得了 1234567...,无论发生什么,如果有东西调用,它都只会将数字 1234567... 传递给其第二个命令。

再次提醒,它从FETCH_HEAD中获取该数字。所以这里重要的是FETCH_HEAD的(完整)内容,这由你是否使用-a / --append进行获取决定。只有在特殊情况下才需要/想要使用--append(当你从多个独立的仓库中获取或为了调试目的而分步获取等)。

当然,稍后会很重要

如果你想要/需要更新你的last-build标签,你将不得不在某个时候运行git fetch --tags --force,现在我们进入原子性问题。

假设您已经运行了git fetch,带有或不带有--tags--force参数,也许是通过运行git pull(不带--tags参数)来运行的。现在,您在本地拥有提交1234567...,名称last-build指向8888888...(未更新)或1234567...(已更新)之一。然后,您运行git fetch --tags --force以更新所有内容。现在,远程可能已经再次移动了last-build。如果是这样,您将获得新值,并更新本地标签。
使用此序列,您可能从未看到过8888888...。您可能有一个包含该提交的分支,但不知道该提交的标记 - 现在您正在更新标记,因此您也无法用该标记知道8888888...。这是好事、坏事还是无关紧要?这取决于您。

避免使用git pull

git pull仅运行git fetch后跟第二个命令,你可以自己运行git fetch,然后再运行第二个命令。这样可以完全控制fetch步骤,并避免重复的获取。

由于您可以控制fetch步骤,因此可以使用refspecs精确地指定要更新的内容。现在是时候访问奇怪的混合标记更新机制了。

取任何一个可用的存储库并运行git ls-remote。这将显示连接时git fetch看到的内容:

$ git ls-remote | head
From git://git.kernel.org/pub/scm/git/git.git
3313b78c145ba9212272b5318c111cde12bfef4a    HEAD
ad36dc8b4b165bf9eb3576b42a241164e312d48c    refs/heads/maint
3313b78c145ba9212272b5318c111cde12bfef4a    refs/heads/master
af746e49c281f2a2946222252a1effea7c9bcf8b    refs/heads/next
6391604f1412fd6fe047444931335bf92c168008    refs/heads/pu
aa3afa0b4ab4f07e6b36f0712fd58229735afddc    refs/heads/todo
d5aef6e4d58cfe1549adef5b436f3ace984e8c86    refs/tags/gitgui-0.10.0
3d654be48f65545c4d3e35f5d3bbed5489820930    refs/tags/gitgui-0.10.0^{}
33682a5e98adfd8ba4ce0e21363c443bd273eb77    refs/tags/gitgui-0.10.1
729ffa50f75a025935623bfc58d0932c65f7de2f    refs/tags/gitgui-0.10.1^{}

您的Git从远程Git获取所有引用及其目标的列表。对于(带注释的)标签,这还包括标记对象的最终目标:这里是gitgui-0.10.0^{}。此语法表示一个已剥离的标签(参见gitrevisions,尽管此处没有使用“peeled”一词)。
然后,默认情况下,您的Git通过请求它们指向的提交以及完成这些提交所需的任何其他提交和对象来获取每个分支(即所有命名为refs/heads/*的内容)。 (您无需下载已经拥有的对象,只需要缺少但需要的对象。)然后,您的Git可以查看所有已剥离的标签,以查看这些标签是否指向其中之一提交。如果是这样,您的Git将使用给定的标签(取决于您的获取方式是否使用--force模式)。如果该标签指向标记对象而不是直接指向提交,则您的Git也会将该标记对象添加到集合中。
在Git 1.8.2之前的版本中,Git错误地将分支规则应用于已推送的标签更新:只要结果是快进,则允许它们不使用--force。也就是说,先前的标签目标只需要是新标签目标的祖先。显然,这仅影响轻量级标签,在任何情况下,Git 1.8.2及更高版本在push上有“永远不要替换标签而不使用--force”的行为。然而,观察到Git 2.10.x和2.11.x的行为是,在使用--tags时,在获取(fetch)时会替换标签。
但是无论如何,如果你的目标是强制更新所有标签和所有远程跟踪分支,git fetch --tags --force --prune 就可以实现;或者你可以使用 git fetch --prune '+refs/tags/*:refs/tags/*' '+refs/heads/*:refs/remotes/origin/*',它使用了 + 语法来强制更新标签和远程跟踪分支。(像往常一样,--prune 是可选的)。强制标志可能是不必要的,但在这里至少是无害的,并且在某些 Git 版本中可能会有用。现在你的标签和远程跟踪分支已经更新,你可以使用 git mergegit rebase 没有任何参数,来合并或变基使用当前分支配置的上游。你可以为尽可能多的分支重复此操作,根本不需要运行 git pull(带有冗余的 fetch)。

1
据我所知(并在git 2.11.0上进行了测试),即使没有“--force”选项,git fetch --tags命令也会始终更新本地标签。 - LeGEC
@LeGEC:有趣。测试表明2.10.1上的行为相同。然而,--tags添加的内置refspec没有设置force位(它是预解析的;请参见remote.c中的s_tag_refspec)。重新阅读文档后,我发现另一个怪异之处:显式refspec将标签提交给--prune,但据说--tags不会这样做。(从源代码中完全不清楚这是如何工作的。) - torek
我还没有在代码中跟踪整个fetch --tags的执行路径,但您可以查看检查tags选项值(在fetch.c中)或关于以refs/tags/开头的引用的硬编码规则的所有位置。标签和分支的处理方式不同。 - LeGEC
我确实查看了所有的代码,但仍然看不出标签在哪里有效地强制更新。代码在某些地方变得相当奇怪,例如本地和远程引用在某一点上看起来像是交换了。对于pushfetch,有单独的硬编码规则,允许在push期间快进分支,但不允许在push期间移动标签。里面相当混乱。 :-) - torek

6

关于顺序:任何顺序都可以(它满足交换律)。


关于您运行的命令:

  • git fetch --tags将已经“强制更新”了您的本地标签
  • --force选项仅适用于不以+选项开头的refspec
  • git pull --tags origin mybranch将一次性应用您想要的所有内容(获取所有标记,并更新您的本地分支)

2
我将回答以下问题(你没有明确提出):
如何在每次调用git fetch或git pull时自动更新一组固定的标签?
我们在我的公司也遇到了完全相同的情况,这是我处理它的方式。
默认情况下,远程引用规范是:
[remote "origin"]
    url = git@server:repo # or whatever
    fetch = +refs/heads/*:refs/remotes/origin/*

这就是为什么它只从远程获取分支 - 它只从远程获取refs/heads/*引用。
这是默认配置,但您可以添加任何您认为合适的引用。
您可以使用refspec告诉git还从远程获取refs/tags/last-build,并自动更新本地标签:
[remote "origin"]
    url = git@server:repo # or whatever
    fetch = +refs/heads/*:refs/remotes/origin/*
    fetch = +refs/tags/last-build:refs/tags/last-build
    # this line tells :
    #   - get the 'refs/tags/last-build' (first ref, before ':') from the remote
    #   - store it in my local tag (second ref after, ':')
    #   - allow forced updates (initial '+')

警告:这行代码会在每次获取时清除您的本地last-build标签,并且git不会为标签保留reflog。考虑到这些标签的含义,我认为这种行为是可以接受的。

如果您感到不舒服,可以指定另一个本地引用:

 # you will see two tags 'last-build' and 'origin/last-build' in your repo :
 fetch = +refs/tags/last-build:refs/tags/origin/last-build

显然,对于每个相关标签,都要添加这样一行...。
参考文献:refspec文档

那个未被询问的问题可以通过git fetch --tags --force来解决,无需进行额外配置。问题在于是否应该在拉取之前或之后执行这样的获取操作。据我所知,根据您的配置,不需要进行额外的获取操作,因为那会影响到拉取。无论如何,问题仍然是使用fetch .. --force... 获取操作应该在拉取之前还是之后执行? - yucer
“git fetch --tags” 的问题在于它会下载所有远程标签,并强制更新 所有 本地标签到这些值。也许这不是你想要的。本答案描述了一种只强制更新您选择的标签的方法。 - LeGEC

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