文档是谎言。您无法从提交中找到标签。您可以从标签中找到提交,这就是git describe
真正的作用,以一种非常曲折的方式,我们稍后会看到。
谎言是一个有用的、描述性的谎言。我认为,它在这方面的成功程度值得商榷。让我们看看git describe
真正的工作原理(不要太深入细节)。不过,首先,我们可能需要一些背景知识。
背景(如果您知道所有这些,请跳到下一部分)
在我们开始之前您需要了解的内容:
有两种“类型”的标签:带注释的标签和轻量级标签。带注释的标签是由git tag -a
or制作的git tag -m
,实际上有两部分:它是一个轻量级标签加上一个实际的 Git 对象,我们稍后会介绍。
默认情况下,git describe
仅查看带注释的标签。使用--tags
使其查看所有标签。
标签是更通用实体的一种特定形式,简称reference或ref。像这样的分支名称master
也是引用,并且git describe
允许使用任何引用,通过--all
.
您还可以使用提交图从提交中找到提交。
作为上述所有内容的基础,Git 既有引用又有对象。这些存储在两个独立的数据库中。1 一个存储名称,所有的形式refs/...
,映射到一个哈希值(SHA-1 目前,虽然 SHA-256 正在计划中)。另一种是由哈希值索引的简单键值存储。所以:
refs objects
+--------------------------------+ +----------------------+
| refs/heads/master a123456... | | 08aef31... <object> |
| refs/tags/v1.2 b789abc... | | a123456... <object> |
+--------------------------------+ | b789abc... <object> |
| <lots more of these> |
+----------------------+
对象数据库通常比参考数据库大得多。
里面其实有四种对象:commit对象、tree对象、blob对象、tag对象。每个对象都有一个对象 ID或 OID,它实际上只是一个哈希(同样,目前是 SHA-1,最终是 SHA-256;将其称为 OID 背后的想法是与最终的转换隔离)。 Blob对象包含 Git 本身不解释的数据。2 所有其他人都持有 Git 至少可以使用的数据。
提交和标记对象是这里特别有趣的对象,因为标记对象包含作为标记目标的 OID ,而提交对象包含提交的每个父级的 OID 。
提交引用(refs/heads/master
等)被限制为仅包含提交对象的 OID。提交对象的父 OID 同样受到限制:每个必须是另一个提交对象的 OID。任何提交的父级都是在创建该特定提交时存在的一些较旧的提交。
如果我们要查看存储库中的所有对象(例如,git gc
做git fsck
),我们可以构建所有提交对象的图,单向箭头从每个提交链接到其所有父对象。如果我们放大一个特定的双父提交,我们可能会看到:
... <commit> <-- +--------+
| commit | <-- <commit> ...
... <commit> <-- +--------+
缩小,我们看到一个整体的有向无环图或所有提交的DAG。同时,存储在分支名称中的 OID(以及保存提交哈希的任何其他引用中)充当此图的入口点,我们可以从这里开始,然后继续关注父链接。
带注释的标签是一个标签引用——或多或少的轻量级标签——它指向一个标签对象。如果基础标记对象然后指向一个提交,那么它也可以作为提交 DAG 的入口点。但是,允许标记对象直接指向树或 blob,或其他标记对象。剥离标签的过程是指跟随指向另一个标签对象的注释标签。我们只是继续跟踪,直到我们到达一些非标签对象:这是这个分层标签的最终目标。如果最终目标是提交,那么这就是 DAG 的另一个入口点。
所以,最后,我们通常有一个分支名称,master
它指向一个主要是线性的提交字符串中的最后一个提交:
... <-o <-o <-o <-o <--master
内部箭头都指向后的事实通常不是很有趣,尽管它会影响git describe
,所以我将它包含在此处。
在存储库生命周期中的不同时间,我们选择一个提交并为其添加一个标签,无论是轻量级的还是带注释的。如果它是带注释的标签,则有一个实际的标签对象:
tag:v1.1 tag:v1.2
| |
v v
T T
| |
v v
... <-o <-o <-o <-o <--master
其中o
s 是提交对象,T
s 是标记对象。
1参考数据库非常俗气:它实际上只是一个平面文件.git/packed-refs
,加上一堆单独的文件和子目录,.git/refs/**/*
. 尽管如此,在 Git 内部,有一个用于添加新数据库的插件接口,并且考虑到平面文件和单个文件的所有问题,我希望最终会有一个真正的数据库作为选项。
2大多数情况下,这是您自己的文件数据。例如,对于符号链接,符号链接的目标存储为 blob 对象,因此数据随后会由您的主机操作系统解释。
git describe
工作原理
该git describe
命令想要找到一些名称——通常是一些带注释的标记对象——这样您要求描述的提交是标记提交的后代。也就是说,标签可以直接指向提交 X,或者指向作为 X 的直接父级的提交(后退一步),或者指向从 X 后退一些步的提交,希望不会太多。
在 Git 中,很难找到某个特定提交的后代。但是很容易找到某个特定提交的祖先。因此,Git 不必从每个标记开始并向前工作,而是必须从提交 X 开始并向后工作。X 本身是由某个标签描述的吗?如果不是,请尝试 X 的每个父母:他们是某个标签的直接目标吗?如果不是,请尝试 X 的每个祖父母:他们是某个标签的直接目标吗?
因此git describe
它会找到所有或至少一些有趣引用(带注释的标签,或所有标签,或所有引用)的目标。当它在我们的示例中执行此“有趣的 refs”时,它会找到两个提交,我们将用 标记*
:
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>
where 是指向哪个提交的缩写 OID master
。
这个图表——图表本身和两个带注释的标签——都非常简单。由于分支和合并,大多数真实的图都非常打结。即使我们只画另一个相当简单的,我们也可以得到这样的东西:
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 没有简单的方法来加载整个图并进行广度优先搜索,而且代码非常广告特设的。但是,很明显这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
请注意, commit的b9436bdf
尖端没有commit作为祖先。Tag指向对象,该对象是一个带注释的标签对象,而该对象又指向 commit :origin/1.5.4.1-final
81ec3665
1.5.4.1-master_20161201
4e415462
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
,不符合描述 commit的条件b9436bdf
。在这个特定的图表中没有提交是 commit 的后代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
应该和确实列出的内容:
$ git describe b96e3147
2.0.0-master_20170202
最终的问题是,任何给定的提交对之间都没有简单的“祖先/后代”关系。 一些提交确实有这样的关系。其他人只是兄弟姐妹:他们有一些共同的祖先。如果您有一个包含多个根提交的图表,还有一些可能没有共同的祖先。
在任何情况下,git describe
通常都需要与内部箭头的方向相反:它必须找到一个标记的提交,以便要描述的提交是该标记的后代。它实际上无法做到这一点,因此它将问题转化为它可以解决的问题:从所有标记提交的集合中找到一些标记提交,这样标记提交是所需提交的祖先——然后,计数从所需提交向后移动到此标记提交所需的跃点数。