7. Refs, tags and branches 引用、标签和分支
7.1. What a ref is, and the show-ref command 什么是引用,以及 show-ref 命令
As of now, the only way we can refer to objects is by their full hexadecimal identifier. In git, we actually rarely see those, except to talk about a specific commit. But in general, we’re talking about HEAD, about some branch called names like main
or feature/more-bombs
, and so on. This is handled by a simple mechanism called references.
到目前为止,我们引用对象的唯一方式是通过它们的完整十六进制标识符。在 Git 中,实际上我们很少直接看到这些标识符,除非是在谈论特定的提交。但通常情况下,我们讨论的是 HEAD,或者一些名为 main
或 feature/more-bombs
的分支等等。这一切都是通过一种简单的机制称为引用来实现的。
Git references, or refs, are probably the most simple type of things git holds. They live in subdirectories of .git/refs
, and are text files containing a hexadecimal representation of an object’s hash, encoded in ASCII. They’re actually as simple as this:
Git 引用,简称 refs,可能是 Git 中保存的最简单类型的对象。它们位于 .git/refs
的子目录中,包含以 ASCII 编码的对象哈希的十六进制表示。这些引用实际上就是这样简单:
6071c08bcb4757d8c89a30d9755d2466cef8c1de
Refs can also refer to another reference, and thus only indirectly to an object, in which case they look like this:
此外,refs 还可以引用另一个引用,从而间接地引用一个对象,在这种情况下,它们的格式如下:
ref: refs/remotes/origin/master
备注
Direct and indirect references直接引用和间接引用
From now on, I will call a reference of the form ref: path/to/other/ref
an indirect reference, and a ref with a SHA-1 object ID a direct reference.
从现在开始,我将把形如 ref: path/to/other/ref
的引用称为间接引用,而带有 SHA-1 对象 ID 的引用称为直接引用。
This section will describe the uses of refs. For now, all that matter is this:
本节将描述引用的用途。现在,重要的是以下几点:
- they’re text files, in the
.git/refs
hierarchy; - 它们是位于
.git/refs
目录中的文本文件; - they hold the SHA-1 identifier of an object, or a reference to another reference, ultimately to a SHA-1 (no loops!)
- 它们保存一个对象的 SHA-1 标识符,或者对另一个引用的引用,最终指向一个 SHA-1(没有循环!)
To work with refs, we’re first going to need a simple recursive solver that will take a ref name, follow eventual recursive references (refs whose content begin with ref:
, as exemplified above) and return a SHA-1 identifier:
为了处理引用,我们首先需要一个简单的递归解析器,它将接受一个引用名称,跟踪可能的递归引用(内容以 ref:
开头的引用,如上所示),并返回一个 SHA-1 标识符:
def ref_resolve(repo, ref):
path = repo_file(repo, ref)
# Sometimes, an indirect reference may be broken. This is normal
# in one specific case: we're looking for HEAD on a new repository
# with no commits. In that case, .git/HEAD points to "ref:
# refs/heads/main", but .git/refs/heads/main doesn't exist yet
# (since there's no commit for it to refer to).
# 有时,间接引用可能会损坏。这在一种特定情况下是正常的:
# 我们在一个没有提交的新仓库中查找 HEAD。在这种情况下,
# .git/HEAD 指向 "ref: refs/heads/main",但 .git/refs/heads/main
# 还不存在(因为没有提交可以引用它)。
if not os.path.isfile(path):
return None
with open(path, 'r') as fp:
data = fp.read()[:-1]
# Drop final \n ^^^^^
# 去掉最后的 \n ^^^^^
if data.startswith("ref: "):
return ref_resolve(repo, data[5:])
else:
return data
Let’s create two small functions, and implement the show-refs
command — it just lists all references in a repository. First, a stupid recursive function to collect refs and return them as a dict:
让我们创建两个小函数,并实现 show-refs
命令——它只是列出一个仓库中的所有引用。首先,一个简单的递归函数来收集引用并将其作为字典返回:
def ref_list(repo, path=None):
if not path:
path = repo_dir(repo, "refs")
ret = collections.OrderedDict()
# Git shows refs sorted. To do the same, we use
# an OrderedDict and sort the output of listdir
# Git 显示的引用是排序的。为了实现同样的效果,我们使用
# OrderedDict 并对 listdir 的输出进行排序
for f in sorted(os.listdir(path)):
can = os.path.join(path, f)
if os.path.isdir(can):
ret[f] = ref_list(repo, can)
else:
ret[f] = ref_resolve(repo, can)
return ret
And, as usual, a subparser, a bridge, and a (recursive) worker function:
和往常一样,我们需要一个子解析器,一个桥接函数,以及一个(递归)工作函数:
argsp = argsubparsers.add_parser("show-ref", help="列出引用。")
def cmd_show_ref(args):
repo = repo_find()
refs = ref_list(repo)
show_ref(repo, refs, prefix="refs")
def show_ref(repo, refs, with_hash=True, prefix=""):
for k, v in refs.items():
if type(v) == str:
print("{0}{1}{2}".format(
v + " " if with_hash else "",
prefix + "/" if prefix else "",
k))
else:
show_ref(repo, v, with_hash=with_hash, prefix="{0}{1}{2}".format(prefix, "/" if prefix else "", k))
7.2. Tags as references 标签作为引用
The most simple use of refs is tags. A tag is just a user-defined name for an object, often a commit. A very common use of tags is identifying software releases: You’ve just merged the last commit of, say, version 12.78.52 of your program, so your most recent commit (let’s call it 6071c08
) is your version 12.78.52. To make this association explicit, all you have to do is:
引用的最简单用法就是标签。标签只是对象(通常是提交)的用户定义名称。标签的一个常见用途是标识软件版本:假设你刚刚合并了你程序的版本 12.78.52 的最后一次提交,所以你最近的提交(我们称之为 6071c08
)就是 你的版本 12.78.52。为了明确这个关联,你只需执行以下命令:
git tag v12.78.52 6071c08
# the object hash ^here^^ is optional and defaults to HEAD.
# 此处的对象哈希 ^可选,默认为 HEAD。
This creates a new tag, called v12.78.52
, pointing at 6071c08
. Tagging is like aliasing: a tag introduces a new way to refer to an existing object. After the tag is created, the name v12.78.52
refers to 6071c08
. For example, these two commands are now perfectly equivalent:
这将创建一个名为 v12.78.52
的新标签,指向 6071c08
。标签就像别名:标签为现有对象提供了一种新的引用方式。创建标签后,名称 v12.78.52
就指向 6071c08
。例如,这两个命令现在是完全等效的:
git checkout v12.78.52
git checkout 6071c08
备注
Versions are a common use of tags, but like almost everything in Git, tags have no predefined semantics: they mean whatever you want them to mean, and can point to whichever object you want, you can even tag blobs!
版本是标签的一个常见用途,但就像 Git 中几乎所有事物一样,标签没有预定义的语义:它们可以根据你的需求而定,并可以指向任何你想要的对象,甚至可以给 blob 打标签!
7.3. Lightweight tags and tag objects, and parsing the latter 轻量标签和标签对象,以及解析标签对象
You’ve probably guessed already that tags are actually refs. They live in the .git/refs/tags/
hierarchy. The only point worth noting is that they come in two flavors: lightweight tags and tags objects.
你可能已经猜到了,标签实际上就是引用。它们位于 .git/refs/tags/
目录中。唯一值得注意的是,标签有两种类型:轻量标签和标签对象。
“Lightweight” tags
are just regular refs to a commit, a tree or a blob.轻量标签
只是指向提交、树或 blob 的常规引用。Tag objects
are regular refs pointing to an object of typetag
. Unlike lightweight tags, tag objects have an author, a date, an optional PGP signature and an optional annotation. Their format is the same as a commit object.标签对象
是指向类型为tag
的对象的常规引用。与轻量标签不同,标签对象具有作者、日期、可选的 PGP 签名和可选的注释。它们的格式与提交对象相同。
We don’t even need to implement tag objects, we can reuse GitCommit
and just change the fmt
field:
我们甚至不需要实现标签对象,可以重用 GitCommit
并只需更改 fmt
字段:
class GitTag(GitCommit):
fmt = b'tag'
And now we support tags.
现在我们就支持标签了。
7.4. The tag command | tag 命令
Let’s add the tag
command. In Git, it does two things: it creates a new tag or list existing tags (by default). So you can invoke it with:
让我们添加 tag
命令。在 Git 中,它有两个功能:创建一个新标签或列出现有标签(默认情况下)。因此,你可以这样调用它:
git tag # List all tags
git tag NAME [OBJECT] # create a new *lightweight* tag NAME, pointing
# at HEAD (default) or OBJECT
git tag -a NAME [OBJECT] # create a new tag *object* NAME, pointing at
# HEAD (default) or OBJECT
git tag # 列出所有标签
git tag NAME [OBJECT] # 创建一个新的 *轻量* 标签 NAME,指向
# HEAD(默认)或 OBJECT
git tag -a NAME [OBJECT] # 创建一个新的标签 *对象* NAME,指向
# HEAD(默认)或 OBJECT
This translates to argparse as follows. Notice we ignore the mutual exclusion between --list
and [-a] name [object]
, which seems too complicated for argparse.
这在 argparse 中的翻译如下。请注意,我们忽略了 --list
和 [-a] name [object]
之间的互斥关系,因为这对 argparse 来说似乎太复杂了。
argsp = argsubparsers.add_parser(
"tag",
help="List and create tags")
argsp.add_argument("-a",
action="store_true",
dest="create_tag_object",
help="Whether to create a tag object")
argsp.add_argument("name",
nargs="?",
help="The new tag's name")
argsp.add_argument("object",
default="HEAD",
nargs="?",
help="The object the new tag will point to")
argsp = argsubparsers.add_parser(
"tag",
help="列出和创建标签")
argsp.add_argument("-a",
action="store_true",
dest="create_tag_object",
help="是否创建标签对象")
argsp.add_argument("name",
nargs="?",
help="新标签的名称")
argsp.add_argument("object",
default="HEAD",
nargs="?",
help="新标签将指向的对象")
The cmd_tag
function will dispatch behavior (list or create) depending on whether or not name
is provided.
cmd_tag
函数将根据是否提供 name
来分发行为(列出或创建)。
def cmd_tag(args):
repo = repo_find()
if args.name:
tag_create(repo,
args.name,
args.object,
type="object" if args.create_tag_object else "ref")
else:
refs = ref_list(repo)
show_ref(repo, refs["tags"], with_hash=False)
And we just need one more function to actually create the tag:
我们只需要再添加一个函数来实际创建标签:
def tag_create(repo, name, ref, create_tag_object=False):
# get the GitObject from the object reference
sha = object_find(repo, ref)
if create_tag_object:
# create tag object (commit)
tag = GitTag(repo)
tag.kvlm = collections.OrderedDict()
tag.kvlm[b'object'] = sha.encode()
tag.kvlm[b'type'] = b'commit'
tag.kvlm[b'tag'] = name.encode()
# Feel free to let the user give their name!
# Notice you can fix this after commit, read on!
tag.kvlm[b'tagger'] = b'Wyag <[email protected]>'
# …and a tag message!
tag.kvlm[None] = b"A tag generated by wyag, which won't let you customize the message!"
tag_sha = object_write(tag)
# create reference
ref_create(repo, "tags/" + name, tag_sha)
else:
# create lightweight tag (ref)
ref_create(repo, "tags/" + name, sha)
def ref_create(repo, ref_name, sha):
with open(repo_file(repo, "refs/" + ref_name), 'w') as fp:
fp.write(sha + "\n")
def tag_create(repo, name, ref, create_tag_object=False):
# 从对象引用获取 GitObject
sha = object_find(repo, ref)
if create_tag_object:
# 创建标签对象(提交)
tag = GitTag(repo)
tag.kvlm = collections.OrderedDict()
tag.kvlm[b'object'] = sha.encode()
tag.kvlm[b'type'] = b'commit'
tag.kvlm[b'tag'] = name.encode()
# 可以让用户提供他们的名字!
# 注意,你可以在提交后修复这个问题,继续阅读!
tag.kvlm[b'tagger'] = b'Wyag <[email protected]>'
# …并添加标签消息!
tag.kvlm[None] = b"由 wyag 生成的标签,无法自定义消息!"
tag_sha = object_write(tag)
# 创建引用
ref_create(repo, "tags/" + name, tag_sha)
else:
# 创建轻量标签(引用)
ref_create(repo, "tags/" + name, sha)
def ref_create(repo, ref_name, sha):
with open(repo_file(repo, "refs/" + ref_name), 'w') as fp:
fp.write(sha + "\n")
7.5. What’s a branch? 什么是分支?
Tags are done. Now for another big chunk: branches.
标签的部分完成了。现在进入另一个重要的部分:分支。
It’s time to address the elephant in the room: like most Git users, wyag still doesn’t have any idea what a branch is. It currently treats a repository as a bunch of disorganized objects, some of them commits, and has no representation whatsoever of the fact that commits are grouped in branches, and that at every point in time there’s a commit that’s HEAD
, ie, the head commit (or “tip”) of the active branch.
是时候解决这个关键问题了:和大多数 Git 用户一样,wyag 目前对分支的概念仍然模糊。它将一个仓库视为一堆无序的对象,其中一些是提交,但完全没有表示提交是如何分组在分支中的,以及在任何时刻都有一个提交是 HEAD
,即 活动 分支的 头部 提交(或“尖端”)。
So, what’s a branch? The answer is actually surprisingly simple, but it may also end up being simply surprising: a branch is a reference to a commit. You could even say that a branch is a kind of a name for a commit. In this regard, a branch is exactly the same thing as a tag. Tags are refs that live in .git/refs/tags
, branches are refs that live in .git/refs/heads
.
那么,分支是什么呢?答案实际上出乎意料地简单,但也可能令人惊讶:分支是对提交的引用。你甚至可以说,分支是一种对提交的名称。从这个意义上说,分支与标签是完全一样的。标签是存放在 .git/refs/tags
中的引用,分支是存放在 .git/refs/heads
中的引用。
There are, of course, differences between a branch and a tag:
当然,分支和标签之间是有区别的:
Branches are references to a commit, tags can refer to any object;
Most importantly, the branch ref is updated at each commit. This means that whenever you commit, Git actually does this:
- a new commit object is created, with the current branch’s (commit!) ID as its parent;
- the commit object is hashed and stored;
- the branch ref is updated to refer to the new commit’s hash.
分支是指向 提交 的引用,而标签可以指向任何对象;
最重要的是,分支引用在每次提交时都会更新。这意味着每当你提交时,Git 实际上会执行以下操作:
- 创建一个新的提交对象,其父对象是当前分支的(提交!)ID;
- 哈希化并存储提交对象;
- 更新分支引用,以指向新提交的哈希。
That’s all.
就这些。
But what about the current branch? It’s actually even easier. It’s a ref file outside of the refs
hierarchy, in .git/HEAD
, which is an indirect ref (that is, it is of the form ref: path/to/other/ref
, and not a simple hash).
那么 当前 分支呢?实际上更简单。它是位于 refs
层级之外的一个引用文件,位于 .git/HEAD
,这是一个 间接 引用(即,它的形式是 ref: path/to/other/ref
,而不是简单的哈希)。
备注
Detached HEAD分离的 HEAD
When you just checkout a random commit, git will warn you it’s in “detached HEAD state”. This means you’re not on any branch anymore. In this case, .git/HEAD
is a direct reference: it contains a SHA-1.
当你检出一个随机提交时,Git 会警告你处于“分离的 HEAD 状态”。这意味着你不再处于任何分支中。在这种情况下,.git/HEAD
是一个 直接 引用:它包含一个 SHA-1。
7.6. Referring to objects: the object_find
function 引用对象:object_find
函数
7.6.1. Resolving names 解析名称
Remember when we’ve created the stupid object_find
function that would take four arguments, return the second unmodified and ignore the other three? It’s time to replace it by something more useful. We’re going to implement a small, but usable, subset of the actual Git name resolution algorithm. The new object_find()
will work in two steps: first, given a name, it will return a complete sha-1 hash. For example, with HEAD
, it will return the hash of the head commit of the current branch, etc. More precisely, this name resolution function will work like this:
还记得我们创建的那个“愚蠢的 object_find
函数”吗?它接受四个参数,返回第二个参数不变并忽略其他三个。现在是时候用更有用的东西来替换它了。我们将实现一个小而可用的实际 Git 名称解析算法的子集。新的 object_find()
将分两步工作:首先,给定一个名称,它将返回一个完整的 SHA-1 哈希。例如,使用 HEAD
,它将返回当前分支的头部提交的哈希,等等。更精确地说,这个名称解析函数的工作方式如下:
- If
name
isHEAD
, it will just resolve.git/HEAD
; - 如果
name
是HEAD
,它将解析.git/HEAD
; - If
name
is a full hash, this hash is returned unmodified. - 如果
name
是完整的哈希,则返回该哈希不变。 - If
name
looks like a short hash, it will collect objects whose full hash begin with this short hash. - 如果
name
看起来像一个短哈希,它将收集完整哈希以此短哈希开头的对象。 - At last, it will resolve tags and branches matching name.
- 最后,它将解析与名称匹配的标签和分支。
Notice how the last two steps collect values: the first two are absolute references, so we can safely return a result. But short hashes or branch names can be ambiguous, we want to enumerate all possible meanings of the name and raise an error if we’ve found more than 1.
请注意最后两步是如何 收集 值的:前两步是绝对引用,因此我们可以安全地返回结果。但短哈希或分支名称可能是模糊的,我们希望枚举名称的所有可能含义,并在找到多个结果时抛出错误。
INFO
Short hashes短哈希
For convenience, Git allows to refer to hashes by a prefix of their name. For example, 5bd254aa973646fa16f66d702a5826ea14a3eb45
can be referred to as 5bd254
. This is called a “short hash”.
为了方便,Git 允许通过名称的前缀来引用哈希。例如,5bd254aa973646fa16f66d702a5826ea14a3eb45
可以被称为 5bd254
。这被称为“短哈希”。
def object_resolve(repo, name):
"""Resolve name to an object hash in repo.
This function is aware of:
- the HEAD literal
- short and long hashes
- tags
- branches
- remote branches"""
"""将名称解析为 repo 中的对象哈希。
此函数支持:
- HEAD 字面量
- 短哈希和长哈希
- 标签
- 分支
- 远程分支"""
candidates = list()
hashRE = re.compile(r"^[0-9A-Fa-f]{4,40}$")
# Empty string? Abort.
# 空字符串?终止。
if not name.strip():
return None
# Head is nonambiguous
# HEAD 是明确的
if name == "HEAD":
return [ ref_resolve(repo, "HEAD") ]
# If it's a hex string, try for a hash.
# 如果是十六进制字符串,尝试查找哈希。
if hashRE.match(name):
# This may be a hash, either small or full. 4 seems to be the
# minimal length for git to consider something a short hash.
# This limit is documented in man git-rev-parse
# 这可能是一个哈希,可能是短的或完整的。4 是 Git 认为某个东西是短哈希的最小长度。
# 这个限制在 man git-rev-parse 中有说明。
name = name.lower()
prefix = name[0:2]
path = repo_dir(repo, "objects", prefix, mkdir=False)
if path:
rem = name[2:]
for f in os.listdir(path):
if f.startswith(rem):
# Notice a string startswith() itself, so this
# works for full hashes.
# 注意字符串的 startswith() 本身适用于完整哈希。
candidates.append(prefix + f)
# Try for references.
# 尝试查找引用。
as_tag = ref_resolve(repo, "refs/tags/" + name)
if as_tag: # Did we find a tag? 找到了标签吗?
candidates.append(as_tag)
as_branch = ref_resolve(repo, "refs/heads/" + name)
if as_branch: # Did we find a branch? 找到了分支吗?
candidates.append(as_branch)
return candidates
The second step is to follow the object we found to an object of the required type, if a type argument was provided. Since we only need to handle trivial cases, this is a very simple iterative process:
第二步是跟随我们找到的对象到所需类型的对象,如果提供了类型参数。由于我们只需处理简单的情况,这个过程非常简单且是迭代的:
- If we have a tag and
fmt
is anything else, we follow the tag. - 如果我们有一个标签而
fmt
是其他任何值,我们就跟随这个标签。 - If we have a commit and
fmt
is tree, we return this commit’s tree object - 如果我们有一个提交而
fmt
是 tree,我们返回这个提交的树对象。 - In all other situations, we bail out: nothing else makes sense.
- 在其他情况下,我们退出:没有其他的情况有意义。
(The process is iterative because it may take an undefined number of steps, since tags themselves can be tagged) (这个过程是迭代的,因为可能需要不确定的步骤,因为标签本身可以被标记)
def object_find(repo, name, fmt=None, follow=True):
sha = object_resolve(repo, name)
if not sha:
raise Exception("No such reference {0}.".format(name))
if len(sha) > 1:
raise Exception("Ambiguous reference {0}: Candidates are:\n - {1}.".format(name, "\n - ".join(sha)))
sha = sha[0]
if not fmt:
return sha
while True:
obj = object_read(repo, sha)
# ^^^^^^^^^^^ < this is a bit agressive: we're reading
# the full object just to get its type. And we're doing
# that in a loop, albeit normally short. Don't expect
# high performance here.
if obj.fmt == fmt:
return sha
if not follow:
return None
# Follow tags
if obj.fmt == b'tag':
sha = obj.kvlm[b'object'].decode("ascii")
elif obj.fmt == b'commit' and fmt == b'tree':
sha = obj.kvlm[b'tree'].decode("ascii")
else:
return None
def object_find(repo, name, fmt=None, follow=True):
sha = object_resolve(repo, name)
if not sha:
raise Exception("没有这样的引用 {0}.".format(name))
if len(sha) > 1:
raise Exception("模糊的引用 {0}:候选项为:\n - {1}.".format(name, "\n - ".join(sha)))
sha = sha[0]
if not fmt:
return sha
while True:
obj = object_read(repo, sha)
# ^^^^^^^^^^^ < 这有点激进:我们读取整个对象只是为了获取它的类型。
# 而且我们在一个循环中这样做,尽管通常很短。这里不期望高性能。
if obj.fmt == fmt:
return sha
if not follow:
return None
# 跟随标签
if obj.fmt == b'tag':
sha = obj.kvlm[b'object'].decode("ascii")
elif obj.fmt == b'commit' and fmt == b'tree':
sha = obj.kvlm[b'tree'].decode("ascii")
else:
return None
With the new object_find()
, the CLI wyag becomes a bit more usable. You can now do things like:
通过新的 object_find()
,CLI wyag 变得更加可用。你现在可以做一些这样的事情:
$ wyag checkout v3.11 # A tag
$ wyag checkout feature/explosions # A branch
$ wyag ls-tree -r HEAD # The active branch or commit. There's also a
# follow here: HEAD is actually a commit.
$ wyag cat-file blob e0695f # A short hash
$ wyag cat-file tree master # A branch, as a tree (another "follow")
$ wyag checkout v3.11 # 一个标签
$ wyag checkout feature/explosions # 一个分支
$ wyag ls-tree -r HEAD # 当前分支或提交。这里还有一个跟随:HEAD 实际上是一个提交。
$ wyag cat-file blob e0695f # 一个短哈希
$ wyag cat-file tree master # 一个分支,作为树(另一个“跟随”)
7.6.2. The rev-parse command | rev-parse 命令
Let’s implement wyag rev-parse
. The git rev-parse
commands does a lot, but one of its use cases, the one we’re going to clone, is solving references. For the purpose of further testing the “follow” feature of object_find
, we’ll add an optional wyag-type
argument to its interface.
让我们实现 wyag rev-parse
。git rev-parse
命令做了很多事情,但我们要克隆的用例是解析引用。为了进一步测试 object_find
的“跟随”功能,我们将在其接口中添加一个可选的 wyag-type
参数。
argsp = argsubparsers.add_parser(
"rev-parse",
help="Parse revision (or other objects) identifiers")
argsp.add_argument("--wyag-type",
metavar="type",
dest="type",
choices=["blob", "commit", "tag", "tree"],
default=None,
help="Specify the expected type")
argsp.add_argument("name",
help="The name to parse")
argsp = argsubparsers.add_parser(
"rev-parse",
help="解析修订版(或其他对象)标识符")
argsp.add_argument("--wyag-type",
metavar="type",
dest="type",
choices=["blob", "commit", "tag", "tree"],
default=None,
help="指定预期的类型")
argsp.add_argument("name",
help="要解析的名称")
The bridge does all the job:
桥接函数完成所有工作:
def cmd_rev_parse(args):
if args.type:
fmt = args.type.encode()
else:
fmt = None
repo = repo_find()
print(object_find(repo, args.name, fmt, follow=True))
And it works:
并且它可以正常工作:
$ wyag rev-parse --wyag-type commit HEAD
6c22393f5e3830d15395fd8d2f8b0cf8eb40dd58
$ wyag rev-parse --wyag-type tree HEAD
11d33fad71dbac72840aff1447e0d080c7484361
$ wyag rev-parse --wyag-type tree HEAD
None