使用 git describe Git 在分支上显示正确的标签

Make Git show the correct tag on branches with git describe

git describe 记录为查找

the most recent tag that is reachable from a commit.

来源:git describe --help

我有点不明白如何从提交中准确地访问标记。当 运行 在分支中时,我没有看到我期望的行为,我不明白为什么。

https://github.com/nodemcu/nodemcu-firmware 使用发布方案,其中所有更改都进入 dev 分支,然后定期恢复到 master。发布和带注释的标签是从 master 创建的。 git describe 运行 在 master 上产生了预期的结果。

当 运行 在 dev 时,我得到了两年前创建的标签。

~/Data/NodeMCU/nodemcu-firmware (dev) > git describe
2.0.0-master_20170202-439-ga08e74d9

这是为什么?

类似的情况是我们为某些用户保留的旧版本的(或多或少)冻结分支。

~/Data/NodeMCU/nodemcu-firmware (1.5.4.1-final) > git describe
0.9.6-dev_20150627-953-gb9436bdf

该分支是在这个带注释的标签 https://github.com/nodemcu/nodemcu-firmware/releases/tag/1.5.4.1-master_20161201 and only a handful of commits 登陆该分支之后创建的。

文档是谎言。您无法从提交中找到标签。您 可以 从标签中找到提交,这就是 git describe 真正 所做的,以一种非常曲折的方式,正如我们将一会儿见。

这个谎言意味着是一个有用的描述性谎言。我认为,它在这方面有多成功还有待商榷。让我们看看 git describe 是如何工作的(不要太深入细节)。不过,首先,我们可能需要一些背景知识。

背景(如果你知道这一切,请跳到下一节)​​

开始之前您需要了解的内容:

  • 标签有两种“种类”:带注释的标签和轻量级标签。带注释的标签是由 git tag -agit tag -m 制作的标签,实际上有两部分:它是一个轻量级标签 加上 一个实际的 Git object,我们稍后会讲到。

    默认情况下,git describe 只查看带注释的标签。使用 --tags 使其查看 all 标签。

  • 标签是更一般实体的特定形式,简称referencerefmaster 等分支名称也是引用,git describe 允许使用任何引用,通过 --all.

  • 您还可以使用 提交图.

    从提交中查找提交

支撑以上所有内容,Git 具有 referencesobjects。这些存储在两个单独的数据库中。1 一个存储名称,所有格式为 refs/...,映射到哈希值(当前为 SHA-1,尽管 SHA-256 是计划中)。另一个是由散列值索引的简单 key-value 存储。所以:

       refs                                objects
+--------------------------------+    +----------------------+
| refs/heads/master   a123456... |    | 08aef31...  <object> |
| refs/tags/v1.2      b789abc... |    | a123456...  <object> |
+--------------------------------+    | b789abc...  <object> |
                                      | <lots more of these> |
                                      +----------------------+

object 数据库通常比参考数据库大很多。

里面其实有四种object:commit objects, tree objects、blob objects 和 tag objects。每个 object 都有一个 object ID 或 OID,它实际上只是一个散列(同样,目前是 SHA-1,最终是 SHA-256;调用背后的想法OID 是为了与最终的转换隔离)。 Blob objects 保存 Git 本身不解释的数据。2 所有其他保存数据 Git 至少 做某事

提交和标记 objects 在这里特别有趣,因为标记 objects 包含一个 OID,它是 target标记,并且提交 objects 包含每个提交的 parent 的 OID。

提交引用(refs/heads/master 等)被限制为仅包含提交 object 的 OID。 Commit objects' parent OID 同样受到限制:每个 OID 都必须是另一个 commit object 的 OID。任何提交的 parent(s) 是创建该特定提交时存在的一些旧提交。

如果我们要查看存储库中的所有 object(例如,git gcgit fsck),我们可以构建一个 所有提交 object 的图形 ,每个提交的 one-way 箭头链接到其所有 parent。如果我们放大一个特定的 two-parent 提交,我们可能会看到:

 ... <commit>  <--  +--------+
                    | commit |  <-- <commit> ...
 ... <commit>  <--  +--------+

向后缩小,我们看到所有提交的整体 有向无环图DAG。同时,存储在分支名称中的 OID——以及任何其他包含 commit 散列的引用——作为此图中的 入口点 ,放置在我们可以开始然后继续关注 parent 链接。

一个 注释标签 是一个标签引用——一个轻量级标签,或多或少——指向一个标签 object。如果底层标记 object 然后指向提交,那么它也充当提交 DAG 的入口点。不过,允许标记 object 直接指向树或斑点,或指向其他标记 object。 剥离标签的过程是指跟随一个指向另一个标签object的注释标签。我们一直跟随,直到我们到达一些 non-tag object:这是这个分层标签的最终目标。如果最终目标是提交,那是 DAG 的另一个入口点。

所以,最后,我们通常有一个 分支名称 master 指向 last 提交mostly-linear 提交字符串:

... <-o <-o <-o <-o   <--master

内部箭头都指向后方的事实通常不是很有趣,尽管它会影响 git describe,因此我将其包含在此处。

在存储库生命周期的不同时间,我们选择一个提交并向其添加 标签,轻量级或注释。如果它是带注释的标签,则有一个实际标签 object:

  tag:v1.1  tag:v1.2
      |       |
      v       v
      T       T
      |       |
      v       v
... <-o <-o <-o <-o   <--master

其中 o 是提交 object,T 是标记 object。


1参考数据库pret俗气:它实际上只是一个平面文件,.git/packed-refs,加上一堆单独的文件和 sub-directories,.git/refs/**/*。不过,在 Git 内部,有一个用于添加新数据库的 plug-in 界面,考虑到 flat-file 和单个文件的所有问题,我希望及时会有一个真实数据库作为一个选项。

2大部分是你自己的文件数据。例如,对于符号链接,符号链接的目标存储为 blob object,因此数据随后会被您的主机 OS 解释。


git describe 的工作原理

git describe 命令想要找到一些名字——通常是一些带注释的标签 object——这样你要求描述的提交是一个 后代 标记的提交。也就是说,标签可以直接指向提交 X,或者指向直接 parent X 的提交(向后退一步),或者指向从 X 向后退一些步的提交,但愿不要太多很多。

在 Git 中,很难找到某个特定提交的 后代 。但是很容易找到某个特定提交的祖先。因此,不是从每个标签开始并向前工作,Git必须从提交X开始并向后工作。 X 本身是否由某个标签描述?如果不是,请尝试 X 的每个 parent:它们是某个标记的直接目标吗?如果不是,请尝试 X 的每个 grandparents:它们是某个标签的直接目标吗?

因此 git describe 它找到了所有或至少一些有趣的参考文献(带注释的标签,或所有标签,或所有参考文献)的目标。当它在我们的示例中执行此“有趣的引用”时,它会找到两个提交,我们将用 *:

标记
  tag:v1.1  tag:v1.2
      |       |
      v       v
      T       T
      |       |
      v       v
... <-* <-o <-* <-o   <--master

现在它从我们想要描述的提交开始:master 的提示。从该提交开始,它可以向后 一个 跃点到达从 v1.2 加星号的提交。或者,它可以向后工作 跳来找到从 v1.1.

加星标的提交

由于 v1.2 更“接近”,因此 git describe 将使用带注释的标签名称。与此同时,它 确实 必须从 master 返回一跳。所以输出将是:

v1.2-1-g<hash>

master 指向的提交的缩写 OID 在哪里。

这张图——包括图本身和两个带注释的标签——非常简单。由于分支和合并,大多数真实的图都是非常打结的。即使我们再画一个fairly简单的,我们也可以得到这样的东西:

                  tag-A       tag-B
                    v           v
         o--o--...--o           o--o   <-- branch1
        /            \         /
...-o--o              o--...--o--o   <-- branch2
        \            /
         o--o--...--o
                ^
              tag-C

在这种情况下,tag-A 将“更接近”branch2 的尖端,并且应该是 git describe 所选择的。 git describe 中的实际算法非常复杂,我不清楚它在一些更棘手的情况下选择了哪个标签:Git 没有一种简单的方法来加载整个图并执行 breadth-first 搜索和代码很特别。然而,它 很明显 tag-B 是不合适的,因为它指向一个提交 不能 branch2 并向后工作。

现在我们可以更仔细地查看您的最后一个示例。我克隆了存储库并这样做了:

$ git log --decorate --graph --oneline origin/1.5.4.1-final 1.5.4.1-master_20161201 
* b9436bdf (origin/1.5.4.1-final) Replace unmainted Flasher with NodeMCU PyFlasher
* 46028b25 Fix relative path to firmware sources
* 6a485568 Re-organize documentation
* f03a8e45 Do not verify the Espressif BBS cert
* 1885a30b Add note about frozen branch
* 017b4637 Adds uart.getconfig(0) to get the current uart parameters (#1658)
* 12a7b1c2 BME280: fixing humidity trimming parameter readout bug (#1652)
* c8176168 Add note about how to merge master-drop PRs
* 063cb6e7 Add lua.cross to CI tests. (#1649)
* 384cfbec Fix missing dbg_printf (#1648)
* 79013ae7 Improve SNTP module: Add list of servers and auto-sync [SNTP module only] (#1596)
* ea7ad213 move init_data from .text to .rodata.dram section (#1643)
* 11ded3fc Update collaborator section
* 9f9fee90 add new rfswitch module to handle 433MHZ devices (#1565)
* 83eec618 Fix iram/irom section contents (#1566)
* 00b356be HTTP module can now chain requests (#1629)
* a48e88d4 EUS bug fixes (#1605)
| *   81ec3665 (tag: 1.5.4.1-master_20161201) Merge pull request #1653 from nodemcu/dev-for-drop
| |\  
| |/  
|/|   
* | 85c3a249 Fix Somfy docs
* |   016f289f Merge pull request #1626 from tae-jun/patch-2
|\ \  
| * | 58321a92 Fix typo at rtctime.md
|/ /  
* | 1032e9dd Extract and hoist net receive callbacks

请注意,提交 b9436bdforigin/1.5.4.1-final 的提示, 没有 将提交 81ec3665 作为祖先。标记 1.5.4.1-master_20161201 指向 object 4e415462 这是一个带注释的标记 object,它又指向提交 81ec3665:

$ git rev-parse 1.5.4.1-master_20161201
4e415462bc7dbc2dc0595a8c55d469740d5149d6
$ git cat-file -p 1.5.4.1-master_20161201
object 81ec3665cb5fe68eb8596612485cc206b65659c9
...

您希望找到的标签 1.5.4.1-master_20161201 符合描述提交 b9436bdf 的条件。在此特定图中有 没有 提交是提交 81ec3665.

的后代

使用git log --all --decorate --oneline --graph,我发现在完整图中有一些这样的提交,例如b96e3147:

* | | e7f06395 Update to current version of SPIFFS (#1949)
| | *   c8ac5cfb (tag: 2.1.0-master_20170521) Merge pull request #1980 from node mcu/dev
| | |\  
| |_|/  
|/| |   
* | |   787379f0 Merge branch 'master' into dev
|\ \ \  
| | |/  
| |/|   
| * | 22e1adc4 Small fix in docs (#1897)
| * |   b96e3147 (tag: 2.0.0-master_20170202) Merge pull request #1774 from node mcu/dev
| |\ \  
| * \ \   81ec3665 (tag: 1.5.4.1-master_20161201) Merge pull request #1653 from nodemcu/dev-for-drop
| |\ \ \  
| * | | | ecf9c644 Revert "Next 1.5.4.1 master drop (#1627)"

但是 b96e3147 本身有自己的(带注释的)标签,所以这就是 git describe 应该做的 list:

$ git describe b96e3147
2.0.0-master_20170202

这里的最终问题是,任何给定的提交对之间都没有简单的“祖先/后代”关系。 一些 提交确实有这样的关系。其他人只是兄弟姐妹:他们有一些共同的祖先。还有一些人可能 没有 共同祖先,如果你有一个包含多个根提交的图表。

在任何情况下,git describe 通常需要 反对 内部箭头的方向:它必须找到一个标记的提交,使得 to-be-described commit 是该标记的后代。它实际上 不能 那样做,所以它将问题转化为它可以做到的问题:从所有标记的提交集合中找到一些标记的提交,这样标记的提交是所需提交的祖先 - 然后,计算从所需提交向后移动到此标记提交所需的跃点数。

Git 2.26 (Q1 2020)(从 v2.26.0-rc0 开始)更改了可访问的内容:在具有多个根提交的存储库中的“git describe”有时会放弃寻找最好的标签来描述一个提交太早,已经调整。

describe: don't abort too early when searching tags

Signed-off-by: Benno Evers
Signed-off-by: Junio C Hamano

When searching the commit graph for tag candidates, git-describe will stop as soon as there is only one active branch left and it already found an annotated tag as a candidate.

This works well as long as all branches eventually connect back to a common root, but if the tags are found across branches with no common ancestor

              B
              o----.
                    \
      o-----o---o----x
      A

it can happen that the search on one branch terminates prematurely because a tag was found on another, independent branch.
This scenario isn't quite as obscure as it sounds, since cloning with a limited depth often introduces many independent "dead ends" into the commit graph.

The help text of git-describe states pretty clearly that when describing a commit D, the number appended to the emitted tag X should correspond to the number of commits found by git log X..D.

Thus, this commit modifies the stopping condition to only abort the search when only one branch is left to search and all current best candidates are descendants from that branch.

For repositories with a single root, this condition is always true: When the search is reduced to a single active branch, the current commit must be an ancestor of all tag candidates.
This means that in the common case, this change will have no negative performance impact since the same number of commits as before will be traversed.