章节 ▾ 第二版

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值表示成功; 任何小于 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 使用异常; 它可以引发诸如 ConfigErrorObjectError 之类的错误来指示错误情况。 其次,没有显式的资源释放,因为 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. 用 head 提交的树填充索引,并在路径 newfile.txt 处添加新文件。

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

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

  5. 提交消息。

  6. 创建提交时,您必须指定新提交的父级。 这使用 HEAD 的 tip 作为单个父级。

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

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

Ruby 代码既好又干净,但由于 Libgit2 正在进行繁重的工作,因此该代码也将运行得非常快。 如果您不是 ruby​​ist,我们将在 其他绑定 中介绍一些其他绑定。

高级功能

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# 编写的,并且非常注意使用本机感觉的 CLR API 包装原始 Libgit2 调用。 我们的示例程序如下所示

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

Python 中 Libgit2 的绑定称为 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 和测试; 通常会有一些小型教程和指向进一步阅读的指针。

scroll-to-top