我如何创建了一个类似于SQL的语言,用于在本地Git仓库上运行查询

我如何创造了一个类似SQL的语言来运行对本地Git存储库的查询

大家好!我是一名对低级编程、编译器和工具开发感兴趣的软件工程师。

三个月前,我决定学习Rust编程语言,并构建一个专注于简洁和高效的Git客户端。我开始思考如何构建这个Git客户端,以提供一些独特和有用的功能。

例如,我喜欢GitHub上的分析页面,它告诉你每个开发人员提交了多少次更改,插入或删除了多少行。但是,如果我想获取某个时间段的分析结果,或者按插入的行数而不是提交的次数进行排序怎么办?或者按周或月对它们进行排序呢?

你可以为客户端添加自定义排序选项,对吗?但是我开始思考如何使其更具动态性。这激发了我想知道是否可以在本地的.git文件上运行类似SQL的查询,以便查询任何所需的信息。

所以想象一下,如果你可以在本地的git库上运行像这样的查询:

SELECT name, COUNT(name) AS commit_num FROM commits GROUP BY name ORDER BY commit_num DESC LIMIT 10

我已经使用一个名为GQL(Git Query Language)的项目实现了这个想法。在本文中,我将向您展示我如何设计和实现这个功能。

如何将类似SQL的查询应用于.git文件?

我最初的想法是使用SQLite。但是有一些问题我无法解决。

例如,我无法自定义语法,并且我不想读取.git文件并将其存储在SQLite数据库中,然后执行查询。我希望一切都可以即时运行。

我还希望不仅能够使用SELECT、DELETE和UPDATE命令,还能提供与Git相关的命令,如push、pull等。

我之前创建过各种工具,比如编译器,那为什么不从零开始创建一个类似SQL的语言,并使其即时执行查询,看看是否可行呢?

我如何设计和实现一个全新的查询语言

我想以仅支持SELECT命令为起点,而不包含聚合、分组、连接等高级功能。

所以我计划将查询解析为一个数据结构,以便在其中执行验证和评估(比如类型检查和在出现问题时显示有用的错误信息)。然后,我将将该数据结构传递给求值器,该求值器将在我的.git文件上应用查询。

选择要使用的数据结构

在这种情况下,最适合的数据结构是使用抽象语法树(AST)来表示查询。这是编译器中常用的数据结构,因为它易于修复、易于遍历和组合节点。

在这种情况下,我不需要保留有关查询的所有信息,只需要保留下一步所需的信息(这就是为什么它被称为抽象)。

决定要执行的验证

在这种情况下,最重要的验证是类型检查,以确保每个值都是有效的,并且在正确的位置使用。

例如,如果查询想要将文本乘以其他文本,这是否有效?

SELECT "ONE" * "TWO"

乘法运算符要求两侧都是数字。所以在这种情况下,我希望通知用户他们的查询无效,并尽可能帮助他们理解问题。

那么它该如何工作呢?当我看到一个像*这样的运算符时,你需要检查两边的值,看看它们是否是此运算符的有效类型。如果不是,则报告如下信息:

SELECT "ONE" * "TWO"-------------^错误:运算符`*`需要两侧均为数字类型,但得到了文本。

除了运算符,我知道我需要检查每个标识符是表、字段、函数名称的别名,还是应该未定义。如果例如,一个branches表只包含2个字段,就像下面的示例一样,我还需要报告错误:

Branches {   Text name,   Number commit_count,}

所以我创建了一个包含所有表格和字段的表示的表,这样我可以轻松进行类型检查。如果用户尝试选择在此模式中未定义的字段,则会报告错误:

SELECT invalid_field_name FROM branches-------------^Error:branches表中未定义字段`invalid_field_name`。

我必须确保对条件、函数名称和参数执行相同的检查。然后,如果一切都被正确定义并且具有正确的类型,AST将是有效的,我们可以进行下一步。

验证抽象语法树后会发生什么?

确保一切有效后,就可以评估查询及其获取结果的方式。

为此,我只需遍历语法树并评估每个节点。完成后,应该会得到一个正确的结果列表。

让我们逐步了解该过程的工作原理。

例如,在此查询中:

SELECT * FROM branches WHEER name LIKE "%/main" ORDER BY commit_count LIMIE BY 5

AST表示将如下所示:

AbstractSyntaxTree {  Select(*, "branches")   Where(Like(name, "%/main"))  OrderBy(commit_count)  Limit(5) }

现在,我们需要按特定顺序遍历并评估每个节点。我们不能只从头到尾或从尾到头,因为我们需要按照SQL执行顺序执行,以获得相同的结果。

例如,在SQL中,必须在GROUP BY之前执行WHERE语句,而HAVING必须在之后执行。

在上面的示例中,所有内容都按照正确的顺序执行,因此让我们看看每个语句会执行什么操作。

  • Select(*, "branches")

这将从名称为branches的表中选择所有字段,并将它们推送到一个列表中 – 让我们称之为objects。但是我如何从本地存储库中选择它们呢?

有关提交、分支、标签等的所有信息都由Git存储在名为.git的文件夹中的文件中。一种选择是从头开始编写完整的解析器以提取所需的信息。但是使用库来完成这个任务对我来说效果很好。

我决定使用libgit2库来执行此任务。它是Git核心方法的纯C实现,因此您可以读取所需的所有信息并在Rust中使用它。 Rust官方团队创建了一个名为git2的包(Rust库),因此您可以轻松地获取分支信息,如下所示:

let local_branches = repo.branches(Some(BranchType::Local));let remote_branches = repo.branches(Some(BranchType::Remote));let local_and_remote_branches = repository.branches(None);

然后迭代每个分支以获取其信息并像这样存储它:

for branch in local_and_remote_branches {   // 从分支中提取信息并存储}

现在我们得到了将在下一步中使用的所有分支的列表。

  • Where(Like(name, "%/main"))

这将筛选对象列表并删除不符合条件的所有项 – 在我们的情况下,即以“/ main”结尾的项。

  • OrderBy(commit_count)

这将按字段的commit_count值对对象列表进行排序。

  • Limit(5)

这仅获取前五个项,并将其从对象列表中删除。

就是这样!现在我们得到了一个有效的结果,您可以在下面看到:

gql_demo

以下示例是有效的,并且可以正确运行:

SELECT 1SELECT 1 + 2SELECT LEN("Git Query Language")SELECT "One" IN ("One", "Two", "Three")SELECT "Git Query Language" LIKE "%Query%"SELECT commit_count FROM branches WHERE commit_count BETWEEN 0 .. 10SELECT * FROM refs WHERE type = "branch"SELECT * FROM refs ORDER BY typeSELECT * FROM commitsSELECT name, email FROM commitsSELECT name, email FROM commits ORDER BY name DESCSELECT name, email FROM commits WHERE name LIKE "%gmail%" ORDER BY nameSELECT * FROM commits WHERE LOWER(name) = "amrdeveloper"SELECT name FROM commits GROUP By nameSELECT name FROM commits GROUP By name having name = "AmrDeveloper"SELECT * FROM branchesSELECT * FROM branches WHERE is_head = trueSELECT name, LEN(name) FROM branchesSELECT * FROM tagsSELECT * FROM tags OFFSET 1 LIMIT 1

如何同时支持多个仓库的运行

在我发布GQL后,收到了很多人的惊人反馈。还有一些功能请求,比如希望支持多个仓库和按仓库路径进行过滤。

我认为这是个好主意,因为我可以为多个项目进行分析,也可以在多个线程上运行。而且,实现起来似乎也不是很难。

所以,在完成AST的验证步骤后,现在是评估步骤的时间了,但是不是只评估一次,而是对每个仓库评估一次,然后将所有结果合并到一个列表中。

那么,如何支持按仓库路径进行过滤的能力呢?

这很简单。还记得branches表的架构吗?我只需要引入一个名为repository_path的新字段,代表该分支的仓库本地路径,并将其引入其他表中就可以了。

所以最终的架构将如下所示:

Branches {   Text name,   Number commit_count,   Text repository_path,}

现在我们可以运行一个使用这个字段的查询:

SELECT * FROM branches WHERE repository_path LIKE "%GQL"

就是这样! 😉

谢谢阅读!

如果你喜欢这个项目,可以在github.com/AmrDeveloper/GQL上给它一个星星 ⭐。

你可以在github.io/GQL网站上查看如何在不同操作系统上下载和使用该项目。

该项目还没有完成 – 这只是个开始。欢迎大家加入并为项目做出贡献,提出想法或报告错误。


Leave a Reply

Your email address will not be published. Required fields are marked *