章节 ▾ 第二版

A2.2 附录 B:在应用中嵌入 Git - Libgit2

Libgit2

另一个可供你选择的方案是使用 Libgit2。Libgit2 是 Git 的一个无依赖实现,其重点是为在其他程序中使用提供一个友好的 API。你可以在 https://libgit2.org 找到它。

首先,让我们看看 C API 是什么样的。这是一个简要的介绍

// Open a repository
git_repository *repo;
int error = git_repository_open(&repo, "/path/to/repository");

// Dereference HEAD to a commit
git_object *head_commit;
error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
git_commit *commit = (git_commit*)head_commit;

// Print some of the commit's properties
printf("%s", git_commit_message(commit));
const git_signature *author = git_commit_author(commit);
printf("%s <%s>\n", author->name, author->email);
const git_oid *tree_id = git_commit_tree_id(commit);

// Cleanup
git_commit_free(commit);
git_repository_free(repo);

头几行打开一个 Git 仓库。`git_repository` 类型表示一个带有内存缓存的仓库句柄。这是最简单的方法,当你知道仓库工作目录或 `.git` 文件夹的确切路径时可以使用。还有 `git_repository_open_ext`,它包含了搜索选项;`git_clone` 和其相关函数用于创建远程仓库的本地克隆;以及 `git_repository_init` 用于创建全新的仓库。

第二段代码使用 rev-parse 语法(更多信息请参阅 分支引用)来获取 HEAD 最终指向的提交。返回的类型是 `git_object` 指针,它表示仓库 Git 对象数据库中存在的内容。`git_object` 实际上是几种不同类型对象的“父”类型;每种“子”类型的内存布局与 `git_object` 相同,因此可以安全地转换为正确的类型。在本例中,`git_object_type(commit)` 将返回 `GIT_OBJ_COMMIT`,因此可以安全地转换为 `git_commit` 指针。

下一段代码展示了如何访问提交的属性。这里的最后一行使用了 `git_oid` 类型;这是 Libgit2 中 SHA-1 哈希的表示。

从这个示例中,一些模式开始浮现

  • 如果你声明一个指针并将其引用传递给 Libgit2 调用,该调用可能会返回一个整数错误代码。`0` 值表示成功;任何小于此值表示错误。

  • 如果 Libgit2 为你填充了一个指针,你需要负责释放它。

  • 如果 Libgit2 从调用返回一个 `const` 指针,你不需要释放它,但当它所属的对象被释放时,它将变为无效。

  • 编写 C 语言代码有点痛苦。

最后一点意味着在使用 Libgit2 时,你不太可能直接编写 C 语言代码。幸运的是,有许多语言特定的绑定可用,使得从你特定的语言和环境中操作 Git 仓库变得相当容易。让我们看看上面用 Libgit2 的 Ruby 绑定(名为 Rugged,可在 https://github.com/libgit2/rugged 找到)编写的示例。

repo = Rugged::Repository.new('path/to/repository')
commit = repo.head.target
puts commit.message
puts "#{commit.author[:name]} <#{commit.author[:email]}>"
tree = commit.tree

如你所见,代码简洁了许多。首先,Rugged 使用异常;它可以抛出诸如 `ConfigError` 或 `ObjectError` 等异常来表示错误情况。其次,由于 Ruby 是垃圾回收的,所以不需要显式释放资源。让我们看一个稍微复杂一点的例子:从头开始构建一个提交

blob_id = repo.write("Blob contents", :blob) # (1)

index = repo.index
index.read_tree(repo.head.target.tree)
index.add(:path => 'newfile.txt', :oid => blob_id) # (2)

sig = {
    :email => "bob@example.com",
    :name => "Bob User",
    :time => Time.now,
}

commit_id = Rugged::Commit.create(repo,
    :tree => index.write_tree(repo), # (3)
    :author => sig,
    :committer => sig, # (4)
    :message => "Add newfile.txt", # (5)
    :parents => repo.empty? ? [] : [ repo.head.target ].compact, # (6)
    :update_ref => 'HEAD', # (7)
)
commit = repo.lookup(commit_id) # (8)
  1. 创建一个新的 blob,其中包含新文件的内容。

  2. 用头部提交的树填充索引,并在路径 `newfile.txt` 处添加新文件。

  3. 这会在 ODB 中创建一个新树,并将其用于新提交。

  4. 我们为作者和提交者字段使用相同的签名。

  5. 提交消息。

  6. 创建提交时,必须指定新提交的父项。这里使用 HEAD 的顶端作为单个父项。

  7. Rugged(和 Libgit2)可以在进行提交时选择性地更新引用。

  8. 返回值是新提交对象的 SHA-1 哈希,然后你可以使用它来获取一个 `Commit` 对象。

Ruby 代码简洁明了,但由于 Libgit2 承担了繁重的工作,这段代码运行速度也相当快。如果你不是 Ruby 用户,我们会在 其他绑定 中介绍一些其他绑定。

高级功能

Libgit2 具有一些超出核心 Git 范围的功能。一个例子是可插拔性:Libgit2 允许你为几种操作类型提供自定义“后端”,因此你可以以与普通 Git 不同的方式存储内容。Libgit2 允许为配置、引用存储和对象数据库等提供自定义后端。

让我们看看它是如何工作的。下面的代码借鉴自 Libgit2 团队提供的一组后端示例(可在 https://github.com/libgit2/libgit2-backends 找到)。以下是设置对象数据库自定义后端的方法

git_odb *odb;
int error = git_odb_new(&odb); // (1)

git_odb_backend *my_backend;
error = git_odb_backend_mine(&my_backend, /*…*/); // (2)

error = git_odb_add_backend(odb, my_backend, 1); // (3)

git_repository *repo;
error = git_repository_open(&repo, "some-path");
error = git_repository_set_odb(repo, odb); // (4)

请注意,错误已被捕获,但未处理。我们希望您的代码比我们的更好。

  1. 初始化一个空的、作为真正执行工作的“后端”容器的对象数据库(ODB)“前端”。

  2. 初始化一个自定义 ODB 后端。

  3. 将后端添加到前端。

  4. 打开一个仓库,并将其设置为使用我们的 ODB 来查找对象。

但是这个 `git_odb_backend_mine` 是什么?嗯,那是你自己的 ODB 实现的构造函数,你可以在里面做任何你想做的事情,只要你正确填充 `git_odb_backend` 结构体。它_可能_看起来像这样

typedef struct {
    git_odb_backend parent;

    // Some other stuff
    void *custom_context;
} my_backend_struct;

int git_odb_backend_mine(git_odb_backend **backend_out, /*…*/)
{
    my_backend_struct *backend;

    backend = calloc(1, sizeof (my_backend_struct));

    backend->custom_context = …;

    backend->parent.read = &my_backend__read;
    backend->parent.read_prefix = &my_backend__read_prefix;
    backend->parent.read_header = &my_backend__read_header;
    // …

    *backend_out = (git_odb_backend *) backend;

    return GIT_SUCCESS;
}

这里最微妙的限制是 `my_backend_struct` 的第一个成员必须是一个 `git_odb_backend` 结构体;这确保了内存布局符合 Libgit2 代码的预期。其余部分是任意的;这个结构体可以根据你的需要任意大或小。

初始化函数为结构体分配一些内存,设置自定义上下文,然后填充它支持的 `parent` 结构体的成员。请查看 Libgit2 源代码中的 `include/git2/sys/odb_backend.h` 文件,获取完整的调用签名集;你的特定用例将有助于确定你需要支持哪些。

其他绑定

Libgit2 有许多语言的绑定。在这里,我们展示了一个小例子,使用了截至撰写本文时一些更完整的绑定包;许多其他语言也有库,包括 C++、Go、Node.js、Erlang 和 JVM,所有这些都处于不同的成熟阶段。官方的绑定集合可以通过浏览 https://github.com/libgit2 的仓库找到。我们将编写的代码将返回 HEAD 最终指向的提交的提交消息(有点像 `git log -1`)。

LibGit2Sharp

如果你正在编写 .NET 或 Mono 应用程序,那么 LibGit2Sharp (https://github.com/libgit2/libgit2sharp) 就是你正在寻找的。这些绑定是用 C# 编写的,并且经过精心设计,将原始的 Libgit2 调用封装在具有原生感觉的 CLR API 中。我们的示例程序看起来像这样

new Repository(@"C:\path\to\repo").Head.Tip.Message;

对于桌面 Windows 应用程序,甚至还有一个 NuGet 包可以帮助您快速入门。

objective-git

如果你的应用程序运行在 Apple 平台上,你很可能使用 Objective-C 作为你的实现语言。Objective-Git (https://github.com/libgit2/objective-git) 是该环境下 Libgit2 绑定的名称。示例程序看起来像这样

GTRepository *repo =
    [[GTRepository alloc] initWithURL:[NSURL fileURLWithPath: @"/path/to/repo"] error:NULL];
NSString *msg = [[[repo headReferenceWithError:NULL] resolvedTarget] message];

Objective-git 与 Swift 完全兼容,所以如果你已经放弃 Objective-C,也不必担心。

pygit2

Libgit2 在 Python 中的绑定被称为 Pygit2,可以在 https://www.pygit2.org 找到。我们的示例程序

pygit2.Repository("/path/to/repo") # open repository
    .head                          # get the current branch
    .peel(pygit2.Commit)           # walk down to the commit
    .message                       # read the message

延伸阅读

当然,本书不会全面介绍 Libgit2 的功能。如果你想了解更多关于 Libgit2 本身的信息,可以在 https://libgit2.github.com/libgit2 找到 API 文档,并在 https://libgit2.github.com/docs 找到一系列指南。对于其他绑定,请查阅捆绑的 README 和测试;那里通常会有一些小教程和进一步阅读的指引。